《Linux高性能服务器编程》学习总结(五)——Linux网络编程基础API
2021-02-16 17:19
第五章 Linux网络编程基础API
对于网络编程,首先要了解的就是字节序的问题,字节序分为主机字节序和网络字节序,主机字节序又称小端字节序,是低字节存放在地地址,而网络字节序又称大端字节序,是低字节放在高地址。当数据在不同的机器上传播时,就需要统一字节顺序以保证不出现错误。在发送数据前,先将需要转变的数据转成网络字节序再发送,接收时先转成主机字节序再处理,要特别注意的是,即使是本机的两个进程通信,也要考虑字节序的问题,比如JAVA的虚拟机就使用大端字节序。使用如下代码可以查看本机的字节顺序:
1 /************************************************************************* 2 > File Name: 5-1.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Thu 01 Feb 2018 12:28:00 AM PST 6 ************************************************************************/ 7 8 #include9 #include 10 using namespace std; 11 12 void byteorder() { 13 union { 14 short value; 15 char union_bytes[sizeof(short)]; 16 }test; 17 test.value = 0x0102; 18 if((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2)) { 19 printf("big endian\n"); 20 } 21 else if((test.union_bytes[1] == 1) && (test.union_bytes[0] == 2)) { 22 printf("little endian\n"); 23 } 24 else printf("unknown...\n"); 25 } 26 27 int main() { 28 byteorder(); 29 }
在socket网络编程接口中,用来表示socket地址的是结构体sockaddr,但由于其无法容纳所有协议族的信息,所以又有了sockaddr_in、sockaddr_in6和sockaddr_un等专用socket地址结构,在编程使用中只需要将对应的地址结构填好再强转成sockaddr类型即可,这样做的好处就是可以简化socket接口,只需要设立一个通用接口即可提供给不同协议族使用。
我们平常使用的IP地址是点分十进制形式的字符串,但是在网络连接中我们需要将其转换成对应的unsigned int类型的数才能使用,所以,API中为我们提供了几个函数:
1 #include 2 in_addr_t inet_addr(const char* strptr); 3 int inet_aton(const char* cp, struct in_addr* inp); 4 char* inet_ntoa(struct in_addr in); 5 inti net_pton(int af, const char* src, void* dst); 6 const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);
其中前三个只适用于ipv4,而后两个适用于ipv4和ipv6。值得注意的是,inet_ntoa函数是不可重入的,其内部使用了一个静态变量来存储结果,函数返回的是静态内存。所以多次调用这个函数返回的是同一块内存,其多次的值都为最后一次的结果。
socket的本质就是一个文件描述符,下面我们总结一下常用的socket函数:
1 #include2 #include 3 int socket(int domain, int type, int protocol); 4 int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen); 5 int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); 6 int listen(int sockfd, int backlog); 7 int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen); 8 int close(int fd); 9 int shutdown(int sockfd, int howto); 10 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 11 ssize_t send(int sockfd, const void *buf, size_t len, int flags); 12 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen); 13 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
前三个函数较为简单,不再赘述。对于监听函数listen,第二个参数是backlog,表示内核监听队列的最大长度,如果数量超过了这个值,则服务器将不受理新的客户连接,下面我们用一个实验来测试一下这个最大长度和backlog有什么关系:
1 /************************************************************************* 2 > File Name: 5-3.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Thu 01 Feb 2018 02:06:39 AM PST 6 ************************************************************************/ 7 8 #include9 #include 10 #include 11 #include in.h> 12 #include 13 #include 14 #include 15 #include 16 #include 17 #includestring.h> 18 using namespace std; 19 20 static bool stop = false; 21 static void handle_term(int sig) { 22 stop = true; 23 } 24 25 int main(int argc, char **argv) { 26 signal(SIGTERM, handle_term); 27 if(argc 3) { 28 printf("usage: %s ip_address port_number backlog\n", basename(argv[0])); 29 return 1; 30 } 31 32 const char* ip = argv[1]; 33 int port = atoi(argv[2]); 34 int backlog = atoi(argv[3]); 35 36 int sock = socket(AF_INET, SOCK_STREAM, 0); 37 assert(sock >= 0); 38 39 struct sockaddr_in address; 40 bzero(&address, sizeof(address)); 41 address.sin_family = AF_INET; 42 address.sin_port = htons(port); 43 inet_pton(AF_INET, ip, &address.sin_addr); 44 45 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 46 assert(ret != -1); 47 48 ret = listen(sock, backlog); 49 assert(ret != -1); 50 51 while(!stop) { 52 sleep(1); 53 } 54 close(sock); 55 return 0; 56 }
运行服务器,监听12345端口并设定最大监听队列为5。用telnet模拟客户端连接,发现当telnet运行到第7个时就连接不上,隔一段时间后返回连接超时,而此时使用netstat -nt | grep 12345查看,发现有6个已经建立的连接,第7个处于SYN_SENT状态。
综上,我们设定的backlog值加1就是监听队列最大能监听的数量。
对于接受连接的accept函数,它从监听队列中取出一个连接,与其建立连接,而不管其处于ESTABLISHED或CLOSE_WAIT状态,更不关心任何网络变化。
当连接结束时,我们调用close将其关闭,但是close并不总是关闭连接,而是将这个文件描述符的引用计数减1,只有当这个文件描述符的引用计数为0时才会真正关闭。在多进程程序中,一次fork就会使得父进程中打开的文件描述符引用计数加1,所以这种情况下我们就应该对父子进程中的文件描述符都执行一次close。如果要立即终止连接,就可以使用下面的shutdown函数,参数howto的取值分别为SHUT_RD、SHUT_WR和SHUT_RDWR。
接下来的函数是重头戏,分别是TCP和UDP的发送和接收数据函数,先看TCP的,最后一个参数flags提供了一些额外的控制,一般情况下取0即可,或者是若干个宏的逻辑或,常用的有MSG_OOB用来发送和接收紧急数据等。我们来通过一个服务器和客户端的例子来说明如何发送带外数据。
1 /************************************************************************* 2 > File Name: 5-6.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Thu 01 Feb 2018 04:37:10 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char **argv) { 12 if(argc 2) { 13 printf("usage: %s ip_address port_number\n", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 19 struct sockaddr_in server_address; 20 bzero(&server_address, sizeof(server_address)); 21 server_address.sin_family = AF_INET; 22 inet_pton(AF_INET, ip, &server_address.sin_addr); 23 server_address.sin_port = htons(port); 24 25 int sockfd = socket(AF_INET, SOCK_STREAM, 0); 26 assert(sockfd >= 0); 27 if(connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) 0) { 28 printf("connection failed\n"); 29 } 30 else { 31 const char* oob_data = "abc"; 32 const char* normal_data = "123"; 33 send(sockfd, normal_data, strlen(normal_data), 0); 34 send(sockfd, oob_data, strlen(oob_data), MSG_OOB); 35 send(sockfd, normal_data, strlen(normal_data), 0); 36 } 37 close(sockfd); 38 return 0; 39 }
1 服务器端: 2 /************************************************************************* 3 > File Name: 5-7.cpp 4 > Author: Torrance_ZHANG 5 > Mail: 597156711@qq.com 6 > Created Time: Thu 01 Feb 2018 04:44:00 AM PST 7 ************************************************************************/ 8 9 #include"head.h" 10 using namespace std; 11 12 #define BUF_SIZE 1024 13 14 int main(int argc, char **argv) { 15 if(argc 2) { 16 printf("usage: %s ip_address port_number\n", basename(argv[0])); 17 return 1; 18 } 19 const char* ip = argv[1]; 20 int port = atoi(argv[2]); 21 22 struct sockaddr_in address; 23 bzero(&address, sizeof(address)); 24 address.sin_family = AF_INET; 25 inet_pton(AF_INET, ip, &address.sin_addr); 26 address.sin_port = htons(port); 27 28 int sock = socket(AF_INET, SOCK_STREAM, 0); 29 assert(sock >= 0); 30 31 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 32 assert(ret != -1); 33 34 ret = listen(sock, 5); 35 assert(ret != -1); 36 37 struct sockaddr_in client; 38 socklen_t client_addrlength = sizeof(int); 39 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 40 if(connfd 0) { 41 printf("errno is: %d\n", errno); 42 } 43 else { 44 char buffer[BUF_SIZE]; 45 46 memset(buffer, 0, sizeof(buffer)); 47 ret = recv(connfd, buffer, BUF_SIZE - 1, 0); 48 printf("got %d bytes of normal data ‘%s‘\n", ret, buffer); 49 50 memset(buffer, 0, sizeof(buffer)); 51 ret = recv(connfd, buffer, BUF_SIZE - 1, MSG_OOB); 52 printf("got %d bytes of oob data ‘%s‘\n", ret, buffer); 53 54 memset(buffer, 0, sizeof(buffer)); 55 ret = recv(connfd, buffer, BUF_SIZE - 1, 0); 56 printf("got %d bytes of normal data ‘%s‘\n", ret, buffer); 57 58 close(connfd); 59 } 60 close(sock); 61 return 0; 62 }
运行结果如图,从这个实验中可以看出三点:首先,带外数据即紧急数据只能有1字节,因为紧急指针指向的位置是带外数据的下一个字节,而TCP首部并没有紧急数据长度的字段,所以只用紧急数据的最后一个字节作为了带外数据,其余还是普通数据;其次,我们看到第一次和第二次调用了send,但是接收的时候是一同接受了前两次中的普通数据,这就很好地说明了TCP是基于流的协议;最后,我们发现当整个数据报中存在紧急数据的时候,其余普通数据就会被紧急数据分隔开,不能一同读取。
UDP的发送和接收函数与TCP的类似,区别在于多了两个参数用来表示对端的socket地址结构。而这两个函数也可用于面向连接时候,只需要把后两个参数设定为NULL即可。
API中还定义了两个不太常用的通用数据读写系统调用recvmsg和sendmsg,还有sockatmark函数用来判断sockfd是否处于带外标记状态,用getsockname和getpeername函数获取本端和对端的socket地址结构,使用较为简单。
socket选项信息是用来对socket的文件属性进行读取和设置的,其两个函数原型为:
1 #include2 int getsockopt(int sockfd, int level, int option_name, void *option_value, socklen_t* restrict option_len); 3 int setsockopt(int sockfd, int level, int option_name, const void *option_value, socklen_t* option_len);
在参数中,level指定了要操作哪个协议的属性,option_name则指定选项的名字,这两项都有固定的搭配,接下来的option_value和option_len是值的大小和长度。值得注意的是,有些选项需要在TCP连接建立之前就设置好,即在调用listen和connect函数之前就设置,这是因为某些选项是TCP连接时需要互相协商的选项,而调用这两个函数时表示已经开始连接或完成连接。
选项种类有很多,常用的有:SO_REUSEADDR选项用来重用TCP端口;SO_RCVBUF和SO_SNDBUF选项用来设置接收及发送缓冲区大小,SO_RCVLOWAT和SO_SNDLOWAT设置缓冲区的低水位标记,意思是如果缓冲区的可读数据或可写空间大于低水位标记系统才通知应用程序从缓冲区读出数据或写入数据,一般默认低水位标记为1字节;SO_LINGER选项用来控制close在关闭TCP时的行为。
socket也提供了几个网络信息API,分别用来根据主机名和ip地址获取主机信息,根据名称或端口获取服务信息,其函数原型如下:
1 #include2 struct hostent* gethostbyname(const char* name); 3 struct hostent* gethostbyaddr(const void* addr, size_t len, int type); 4 struct servent* getservbyname(const char* name, const char* proto); 5 struct servent* getservbyport(int port, const char* proto);
需要指出的是,以上四个函数都是不可重入的,即非线程安全的,不过netdb.h头文件也给出了他们的可重入版本,就是在函数名后加上_r。
文章标题:《Linux高性能服务器编程》学习总结(五)——Linux网络编程基础API
文章链接:http://soscw.com/index.php/essay/56195.html