网站后台网址在哪输入,销售管理系统实验报告,优秀网站建设方案,最近十大新闻网络套接字编程(一) 文章目录 网络套接字编程(一)预备知识源IP地址和目的IP地址端口号TCP/UDP协议特点网络字节序 socket编程socket常用APIsockaddr结构 简易UDP网络程序服务端创建套接字服务端绑定IP地址和端口号字符型IP地址VS整型IP地址服务端运行客户端创建套接字客户端绑定…网络套接字编程(一) 文章目录 网络套接字编程(一)预备知识源IP地址和目的IP地址端口号TCP/UDP协议特点网络字节序 socket编程socket常用APIsockaddr结构 简易UDP网络程序服务端创建套接字服务端绑定IP地址和端口号字符型IP地址VS整型IP地址服务端运行客户端创建套接字客户端绑定问题启动客户端程序测试 预备知识
源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址。
源IP地址的作用是让接收方能够将数据返回过来。
目的IP地址的作用进行路由选择和确认数据是否传输到指定位置。
端口号 端口号 在了解端口号前要明确一些概念首先网络通信使得网络中的一台主机可以将数据传输给另一台主机其目的是为了让多台主机协同完成工作。其次在网络协议栈中由应用层产生数据交付给下层进行传输。而网络应用层中产生数据的其实是进程进程为了完成一个工作通过网络将数据交付给其他主机的进程从而完成多个进程协同工作因此网络通信的本质是进程间通信。
如下图客户端进程通过网络将数据传输给服务端进程让服务端进程对所得到的数据进行处理从而完成任务 既然网络通信的本质是进程间通信因此在网络传输时必须知道数据要定位接收方进程因此使用端口号定位主机中的一个进程又因为IP地址能够定位网络中的一台主机因此IP地址端口号可以定位网络中的唯一一个进程。完整的IP中包含IP地址和端口号。
端口号(port)作为传输层协议的内容其内容如下
端口号是一个2字节16位的整数。端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。IP地址 端口号能够标识网络上的某一台主机的某一个进程。一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。 不采用进程PID定位进程而采用端口号的原因 网络的管理在操作系统中属于文件管理的范畴而进程PID是属于进程管理的范畴并且网络在文件管理中属于一个单独的模块如果使用进程PID就会将网络管理和进程管理关联起来会使得二者耦合度变高不利于维护。 网络将数据交付给进程的原理 网络的管理在操作系统中属于文件管理的范畴进程的管理属于进程管理的范畴网络要将数据交给进程需要将数据加载到文件的缓冲区中然后让进程使用文件操作读取缓冲区的内容最终得到网络传输过来的数据。
TCP/UDP协议特点
应用层进程主要是从传输层获取网络传输的数据传输层中有两种协议TCP协议、UDP协议。 TCP协议特点 TCP(Transmission Control Protocol 传输控制协议)
传输层协议有连接可靠传输面向字节流
注 可靠传输是指使用TCP协议传输数据时如果数据丢失了会采用重传数据等策略保证接收端接收到数据。 UDP协议特点 UDP(User Datagram Protocol 用户数据报协议)
传输层协议无连接不可靠传输面向数据报
注 不可靠传输是指使用UCP协议传输数据时即使数据丢失了也不会采取任何措施。
说明一下 可靠传输和不可靠传输是TCP协议和UDP协议的特点而不是优缺点因为执行可靠传输一定要付出协议复杂传输时间长等的代价不可靠传输由于协议简单会有传输时间短等的优点。
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏 移地址也有大端小端之分, 网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢? 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可; 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主字节序的转换
#include arpa/inet.huint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);这些函数名很好记,h表示host主机,n表示network网络,l表示32位长整数,s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;。如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。 socket编程
socket常用API
#include sys/types.h
#include sys/socket.h// 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockaddr结构
socket编程中用于定义网络地址的结构体种类分为struct sockaddr_in和struct sockaddr_un其中struct sockaddr_in用于IPv4网络struct sockaddr_un用于Unix域(本地局域网)为了兼容两种结构体采用struct sockaddr作为socket接口的参数在接收参数时会判断前16位地址类型是AF_INET还是AF_UNIX区分使用的是那种结构体。 IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示;包括16位地址类型,16位端口号和32位IP地址.
sockaddr_in的结构
struct sockaddr_in
{sa_family_t _sinfamily;//unsigned short int类型参数in_port_t sin_port;//端口号struct in_addr sin_addr;//IP地址//填充字段unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];
};typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};简易UDP网络程序
为了更好的理解socket编程编写一组简易的UDP网络程序其包含客户端和服务端客户端发送数据给服务端服务端将接收的数据回传给客户端。 服务端创建套接字
我们把服务器封装成一个类当我们定义出一个服务器对象后需要马上初始化服务器而初始化服务器需要做的第一件事就是创建套接字。创建套接字需要使用socket函数
//socket函数所在的头文件和函数声明
#include sys/types.h
#include sys/socket.hint socket(int domain, int type, int protocol);该函数用于创建网络套接字。domain参数 指明数据传输域传入AF_INET为网络通信传入AF_UNIX为本地通信。type函数 指明套接字种类传入SOCK_STREAM为流式套接传入 SOCK_DGRAM 为数据报套接。protocol参数 指明所使用的协议默认为0该函数会自动识别所使用的协议。返回值 调用成功返回一个文件描述符调用失败返回-1错误码被设置。 socket函数属于什么类型的接口 在计算机软硬体系结构中程序员编程形成程序都是在操作系统之上的用户层进行的对应TCP/IP网络协议栈的应用层因此socket函数是操作系统提供属于应用层的系统接口。 socket函数底层做了什么 socket函数是被引用层的进程所调用的而每一个进程在系统层面上都有一个进程地址空间PCBtask_struct、文件描述符表files_struct以及对应打开的各种文件。而文件描述符表里面包含了一个数组fd_array其中数组中的0、1、2下标依次对应的就是标准输入、标准输出以及标准错误。
在调用socket函数后,操作系统会为该进程创建该套接字对应的文件,将其记录在该进程的文件描述符表中: 将数据写入该套接字对应的文件中,该文件刷新缓冲区的数据后,就会将数据写入网卡设备中,网卡会将数据传输出去。 创建套接字 enum
{SOCKET_ERR1
};
class UdpServer
{public:void InitServer(){//创建套接字_sock socket(AF_INET, SOCK_DGRAM, 0);if (_sock 0){std::cerr socket create error: strerror(errno) std::endl;exit(SOCKET_ERR); }}private:int _sock; // 网络文件描述符
};服务端绑定IP地址和端口号
现在套接字已经创建成功了但作为一款服务器来讲如果只是把套接字创建好了那我们也只是在系统层面上打开了一个文件操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡此时该文件还没有与网络关联起来。绑定IP地址和端口号需要使用bind函数
//bind函数所在的头文件和函数声明
#include sys/types.h
#include sys/socket.hint bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);该函数用于绑定套接字的IP地址和端口号。sockfd参数 要接收数据的套接字即调用socket函数返回的文件描述符。addr参数 用于传入一个指向存有目标IP地址和端口号的sockaddr类型指针。addlen参数 sockaddr类型变量的长度。返回值 调用成功返回一个文件描述符调用失败返回-1错误码被设置。云服务不需要调用bind函数绑定指定IP地址因为云服务可以存在多个网卡设备需要让云服务自身制定IP地址。可以在将sockaddr类型中的IP地址字段赋值为INADDR_ANY让云服务绑定任意IP地址。 sockaddr_in的结构 前面提到了使用网络通信时,采用的sockaddr结构中的sockaddr_in结构,sockaddr_in具体的数据结构如下
typedef unsigned short int sa_family_t;
typedef uint16_t in_port_t;typedef uint32_t in_addr_t;
struct in_addr
{in_addr_t s_addr;
};struct sockaddr_in
{sa_family_t _sinfamily;//unsigned short int类型参数in_port_t sin_port;//端口号struct in_addr sin_addr;//IP地址//填充字段unsigned char sin_zero[sizeof (struct sockaddr) -__SOCKADDR_COMMON_SIZE -sizeof (in_port_t) -sizeof (struct in_addr)];
};_sinfamily: 标识该sockaddr结构要进行的是网络通信,还是本地通信.网络通信时赋值为AF_INET,本地通信时赋值为AF_UNIX.sin_port: 16位无符号整型表示的端口号。s_addr 32位无符号正式表示的IP地址。 给进程绑定IP地址和端口号的原理 进程在应用层将要绑定的IP地址和端口号写入到对应的sockaddr结构中然后调用操作系统提供的bind系统接口让操作系统完成进程的IP地址和端口号的绑定工作。 本地端口号和网络字节序的转化 进程执行时定义一个无符号的16位整型port变量记录要绑定的端口号后想要将其写入sockaddr结构前需要调用系统提供的网络字节序接口htons接口将port转换成符合网络字节序的16位端口号完成网络字节序的转化后才能将其写入sockaddr结构中并使用其绑定端口号。 绑定IP地址和端口号 enum
{SOCKET_ERR 1,BIND_ERROR
};
class UdpServer
{public:UdpServer(uint16_t port):_port(port) {}void InitServer(){// 创建套接字_sock socket(AF_INET, SOCK_DGRAM, 0);if (_sock 0){std::cerr socket create error: strerror(errno) std::endl;exit(SOCKET_ERR);}// 绑定IP地址和端口号struct sockaddr_in local;//创建sockaddr结构写入IP地址和端口号memset(local, 0, sizeof(local));local.sin_family AF_INET;local.sin_addr.s_addr INADDR_ANY;//云服务器IP地址赋值local.sin_port htons(_port);socklen_t len sizeof(local);int n bind(_sock, (struct sockaddr*)local, len);//端口号绑定if (n 0){std::cerr bind error: strerror(errno) std::endl;exit(BIND_ERROR);}}private:int _sock; // 网络文件描述符uint16_t _port;//端口号
};字符型IP地址VS整型IP地址
IP地址是由 . 分割由四个部分形成的每个部分的取值范围位[0~255],如果使用字符型记录至少需要12个字节(不记录.)也就是96个比特位如果采用整型记录只需要4个字节也就是32位具体的记录方式是将一个4字节的无符号整形数据按照字节划分成4个部分每个部分都占一个字节的空间而一个字节的空间刚好能记录[0~255]的数据 操作系统提供了字符型IP地址和整形IP地址的转换函数我们直接调用即可 inet_addr函数 inet_addr函数的功能是将字符串IP转换成整数IP。
//inet_addr函数所在的头文件和函数声明
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
in_addr_t inet_addr(const char *cp);该函数使用起来非常简单我们只需传入待转换的字符串IP该函数返回的就是转换后的整数IP。除此之外inet_aton函数也可以将字符串IP转换成整数IP不过该函数使用起来没有inet_addr简单。 inet_ntoa函数 inet_ntoa函数的功能是将整数IP转换成字符串IP。
//inet_ntoa函数所在的头文件和函数声明
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
char *inet_ntoa(struct in_addr in);需要注意的是传入inet_ntoa函数的参数类型是in_addr因此我们在传参时不需要选中in_addr结构当中的32位的成员传入直接传入in_addr结构体即可。
服务端运行
服务器运行起来后需要完成从网络中接收数据和将数据回传给客户端的任务接收数据需要用到recvfrom函数 recvfrom函数 //recvfrom函数所在的头文件和函数声明
#include sys/types.h
#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);该函数的功能是从一个指定的套接字接收数据并将数据存储到指定的缓冲区中。sockfd参数 要接收数据的套接字即调用socket函数返回的文件描述符。buf参数 指向用来存储接收数据的缓冲区。len参数 缓冲区长度即最多可以接收的字节数。flags参数 用于控制recvfrom函数的行为默认为0阻塞读取。src_addr参数 存储发送方的地址信息IP地址和端口号的sockaddr类型变量的地址。addrlen参数 指向存放发送方地址信息的sockaddr类型变量的长度的变量的地址。返回值 成功接收数据时返回接收到的字节数。连接关闭时返回0。发生错误时返回-1并设置errno变量以指示具体的错误原因。
服务端接受数据后要发送数据发送数据需要使用sendto函数 sendto函数 //sendto函数所在的头文件和函数声明
#include sys/types.h
#include sys/socket.hssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);该函数的功能是将指定缓冲区中的数据发送到指定的套接字。sockfd参数 要发送数据的套接字即调用socket函数返回的文件描述符。buf参数 指向要发送数据的缓冲区。len参数 要发送的数据长度字节数。flags参数 用于控制sendto函数的行为默认为0。dest_addr参数 指定目标地址即接收方的地址信息包括IP地址和端口号的sockaddr类型变量的指针。addrlen参数 指定目标地址信息的大小即dest_addr的长度。返回值 成功发送数据时返回成功发送的字节数。发生错误时返回-1并设置errno变量以指示具体的错误原因。 启动服务器函数 启动服务器函数的功能让其从网络中接收数据并将数据回传给客户端。
class UdpServer
{public:void StartServer()//服务端运行{char buffer[128];while (true){struct sockaddr_in peer;socklen_t len sizeof(peer);//必须写明ssize_t n recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)peer, len);if (n 0){buffer[n] 0;}elsecontinue;std::string clientip inet_ntoa(peer.sin_addr);uint16_t clientport ntohs(peer.sin_port);std::cout clientip - clientport send# buffer std::endl;sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)peer, len);}}private:int _sock; // 网络文件描述符uint16_t _port;//端口号
};recvfrom函数接收sockaddr结构 recvfrom函数使用sockaddr结构从网络中接收数据时就是按照网络字节序接收的因此再交给sendto函数发送数据时无需进行网络字节序的转化。 缓冲区问题 使用buffer缓冲区从网络中接收数据时按照C语言规定需要为缓冲区预留一个字节用于存储’\0’。
使用buffer缓冲区向网络中发送数据时无需发送’\0’,因为那是C语言的规定不是网络的规定。
recvfrom函数接收数据时会将网络字节序转换为主机序列写入缓冲区sendto函数向网络发送数据时会将缓冲区数据从主机序列转换成网络字节序发送。 运行服务端 调用服务端类内部的函数进行服务端的初始化并启动服务端。为了给错误启动服务端纠错引入了命令行参数在启动服务端时做纠错提示
void Usage(const char *proc)
{std::cout Usage:\n\t proc port\n std::endl;
}int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);}uint16_t port atoi(argv[1]);std::unique_ptrUdpServer ustr(new UdpServer(port));ustr-InitServer();ustr-StartServer();return 0;
}启动服务端 启动服务端后使用netstat -naup指令查看服务端进程信息 客户端创建套接字
同样的将客户端封装成类在使用客户端时只需要创建类对象然后调用对应的函数即可使用客户端。在创建客户端类对象后的第一步就是初始化客户端在初始化客户端时首先就需要创建套接字
enum
{SOCKET_ERR 1,BIND_ERROR
};
class Udp_Client
{public:void InitClient(){//创建套接字_sock socket(AF_INET, SOCK_DGRAM, 0);if (_sock 0){std::cerr socket create error: strerror(errno) std::endl;exit(SOCKET_ERR);}}private:int _sock;
};客户端绑定问题
使用socket进行网络通信是需要通过IP地址和端口号确定唯一进程然后再进行通信的客户端如果不进行IP地址和端口号的绑定服务端就无法将数据再回传给客户端因此客户端是一定需要绑定IP地址和端口号的。
一台主机上会存在大量的客户端进程如果每个客户端进程都要指定绑定端口号可能会因为客户端端口号冲突造成客户端启动失败的问题并且客户端只要能够实现和服务端进行网络通信的功能即可端口号的具体值并不重要因此客户端不能绑定指定的端口号需要让操作系统来完成客户端端口号的绑定。
服务端是给众多客户端提供网络服务的服务端的端口号如果随意改变客户端就会因为服务端的端口号的改变导致无法找到服务端。因此服务端的端口号一定需要自主绑定。
客户端在首次调用发送数据的系统调用时操作系统会自动选择端口号和自身的IP地址绑定到客户端。
启动客户端 运行客户端函数 运行客户端函数的功能是接受用户输入的数据将其发送给服务端然后接受服务端回传的数据。
class Udp_Client
{public:Udp_Client(std::string serverip, uint16_t serverport):_serverip(serverip), _serverport(serverport){}void StartClient(){while(true){std::cout Please enter message#;std::string message;getline(std::cin, message);struct sockaddr_in peer;//指明服务端IP地址和端口号peer.sin_family AF_INET;peer.sin_addr.s_addr inet_addr(_serverip.c_str());peer.sin_port htons(_serverport);sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr*)peer, sizeof(peer));struct sockaddr_in temp;socklen_t tlen;char buffer[128];ssize_t n recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)temp, tlen);if (n 0){buffer[n] 0;std::cout server echo: buffer std::endl;}}}private:int _sock;std::string _serverip;uint16_t _serverport;
};revfrom函数的注意事项 recvfrom函数最后两个参数src_addr,addrlen都是输出型参数在函数中会进行赋值操作因此不能传空指针。 启动客户端 和服务端相同调用客户端类内部的函数进行客户端的初始化并启动客户端。为了给错误启动客户端纠错引入了命令行参数在启动客户端时做纠错提示
void Usage(const char *proc)
{std::cout Usage:\n\t proc port\n std::endl;
}int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);exit(USAGE_ERROR);}uint16_t port atoi(argv[1]);std::unique_ptrUdpServer ustr(new UdpServer(port));ustr-InitServer();ustr-StartServer();return 0;
}程序测试 本地测试 现在服务端和客户端的代码都已经编写完毕我们可以先进行本地测试此时服务器没有绑定外网绑定的是本地环回。现在我们运行服务器时指明端口号为8080再运行客户端此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1服务端的端口号就是8080。 客户端运行之后提示我们进行输入当我们在客户端输入数据后客户端将数据发送给服务端此时服务端再将收到的数据打印输出这时我们在服务端的窗口也看到我们输入的内容。 此时我们再用netstat命令查看网络信息可以看到服务端的端口是8080客户端的端口是。这里客户端能被netstat命令查看到说明客户端也已经动态绑定成功了这就是我们所谓的网络通信。 网络测试 网络测试和本地测试的方式类似只是网络测试输入的IP地址得是服务端的IP地址 不同于本地测试的是可以使用其他主机访问该服务端只需要让其他主机获取该客户端程序然后在其他主机运行客户端时输入服务端IP地址和端口号即可完成网络通信。