Linux 高性能服务器编程(一)

5. Linux网络编程基础API

5.4 监听socket

代码清单:

c 复制代码
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <signal.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <assert.h>
 #include <stdio.h>
 #include <string.h>
 #include <iostream>
 #include <errno.h>
 ​
 static bool stop = false;
 /*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
 static void handle_term(int sig) {
     stop = true;
 }
 ​
 int main(int argc, char* argv[]) {
     signal(SIGTERM, handle_term);
 ​
     if(argc <= 3) {
         std::cout << "usage: " << argv[0] << " ip_address port_number backlog" << std::endl;
     }
     const char* ip = argv[1];
     int port = atoi(argv[2]);
     int backlog = atoi(argv[3]);
 ​
     /*socket函数:
     domain:指定协议族,这里是IPV4
     type:指定传输层协议,这里是TCP服务
     */
     int sock = socket(PF_INET, SOCK_STREAM, 0);
     assert(sock >= 0);
     /*创建一个IPV4地址*/
     struct sockaddr_in address;
     /*bzero(void* p, size_t n),将指定地址后的n个字节清零*/
     bzero(&address, sizeof(address));
     address.sin_family = AF_INET;
     /*点分十进制字符串转网络字节序列整数,支持IPV4和IPV6*/
     inet_pton(AF_INET, ip, &address.sin_addr);
     /*主机字节序列转化为网络字节序列*/
     address.sin_port = htons(port);
 ​
     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
     std::cout << errno << std::endl; 
     assert(ret != -1);
 ​
     ret = listen(sock, backlog);
     assert(ret != -1);
 ​
     /*循环等待连接,直到有SIGTERM信号将它中断*/
     while(!stop) {
         sleep(1);
     }
 ​
     /*关闭socket*/
     close(sock);
     return 0;
 }

我在两台局域网上的服务器做了实验。开启监听Socket的ip地址为192.168.70.73,端口为12345

充当客户端的服务器ip地址为192.168.70.72

当我在客户端服务器发起了7个连接请求后,可以发现此时只有6个请求建立了连接。

这个是客户端的,所以是SYN_SENT

5.5 接受连接

下面的系统调用从listen监听队列接受一个连接:

arduino 复制代码
 #include <sys/types.h>
 #include <sys/socket.h>
 int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)

addr用来接收被服务器接受连接的远端socket地址

sockfd是执行过listen系统调用的监听socket

accept成功时返回一个新的连接socket,该socket唯一地标识了被接收的这个连接

测试accept连接代码:

c 复制代码
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <signal.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <assert.h>
 #include <stdio.h>
 #include <string.h>
 #include <iostream>
 #include <errno.h>
 ​
 static bool stop = false;
 /*SIGTERM信号的处理函数,触发时结束主程序中的循环*/
 static void handle_term(int sig) {
     stop = true;
 }
 ​
 int main(int argc, char* argv[]) {
     signal(SIGTERM, handle_term);
 ​
     if(argc <= 3) {
         std::cout << "usage: " << argv[0] << " ip_address port_number backlog" << std::endl;
     }
     const char* ip = argv[1];
     int port = atoi(argv[2]);
     int backlog = atoi(argv[3]);
 ​
     /*socket函数:
     domain:指定协议族,这里是IPV4
     type:指定传输层协议,这里是TCP服务
     */
     int sock = socket(PF_INET, SOCK_STREAM, 0);
     assert(sock >= 0);
     /*创建一个IPV4地址*/
     struct sockaddr_in address;
     /*bzero(void* p, size_t n),将指定地址后的n个字节清零*/
     bzero(&address, sizeof(address));
     address.sin_family = AF_INET;
     /*点分十进制字符串转网络字节序列整数,支持IPV4和IPV6*/
     inet_pton(AF_INET, ip, &address.sin_addr);
     /*主机字节序列转化为网络字节序列*/
     address.sin_port = htons(port);
 ​
     int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
     std::cout << "errno number:" << errno << std::endl; 
     assert(ret != -1);
 ​
     ret = listen(sock, backlog);
     assert(ret != -1);
 ​
     /*暂停10秒等待科幻段连接和相关操作(掉线或退出)完成*/
     sleep(20);
     struct sockaddr_in client;
     socklen_t client_addrlength = sizeof(client);
     /*client用来获取被接受连接的远端socket地址*/
     int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
     if(connfd < 0) {
         std::cout << "errno is: " << errno << std::endl;
     }
     else {
         /*接受连接成功则打印出客户端的IP地址和端口号*/
         char remote[INET_ADDRSTRLEN];
         std::cout << "connected with ip: " << inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN)  << 
         " and port: " << ntohs(client.sin_port) << std::endl;
     }
     return 0;
 }

实验发现,即使客户端发起请求后断开连接,也可以正常打印信息。

5.6 发起连接

客户端需要通过以下系统调用来主动与服务器建立连接:

arduino 复制代码
 #include <sys/types.h>
 #Include <sys/socket.h>
 int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen)

sockfd是本地用socket系统调用返回的一个socket;'

serv_addr指向要连接的服务器的socket地址。

connect成功时返回0。一旦成功建立连接,sockfd就唯一的标识了这个连接。客户端可以通过读写sockfd来与服务器通信。

5.7 关闭连接

关闭一个连接实际上就是关闭这个连接对应的socket:

arduino 复制代码
 #include <unistd.h>
 int close(int fd);

不过close并不总是立即关闭这个连接,而是将fd的引用计数减一。当引用计数为0时,才真正地关闭连接。在多进程程序中,一次fork系统调默认将使父进程打开的socket的引用计数加1。

如果要立即关闭连接,则应该使用shutdown系统调用。

arduino 复制代码
 #include <sys/socket.h>
 int shutdown(int sockfd, int howto);

5.8 数据读写

5.8.1 TCP数据读写

对于TCP流数据的读写的系统调用是:

arduino 复制代码
 #include <sys/types.h>
 #Include <sys/socket.h>
 ssize_t recv(int sockfd, void* buf, size_t len, int flags);
 ssize_t send(int sockfd, const void* buf, size_t len, int flags);

recv读取sockfd上的数据,buf和len指定缓冲区的位置和大小,flags用于一些特别设置,通常置0即可。

recv返回实际读取到的数据,它可能小于我们期望的长度,这时候需要多次读取。

send成功时返回实际写入的数据的长度

flags参数的可选值,以下仅列出部分

选项名 含义 SEND RECV
MSG_DONTWAIT 对socket的此次操作是非阻塞的 Y Y
MSG_OOB 发送或者接收紧急数据 Y Y

需要注意的是flags参数只对当前的调用有效

发送代码:

c 复制代码
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <assert.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 ​
 int main( int argc, char* argv[] )
 {
     if( argc <= 2 )
     {
         printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
         return 1;
     }
     const char* ip = argv[1];
     int port = atoi( argv[2] );
 ​
     struct sockaddr_in server_address;
     bzero( &server_address, sizeof( server_address ) );
     server_address.sin_family = AF_INET;
     inet_pton( AF_INET, ip, &server_address.sin_addr );
     server_address.sin_port = htons( port );
 ​
     int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
     assert( sockfd >= 0 );
     if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
     {
         printf( "connection failed\n" );
     }
     else
     {
         printf( "send oob data out\n" );
         const char* oob_data = "abc";
         const char* normal_data = "123";
         send( sockfd, normal_data, strlen( normal_data ), 0 );
         send( sockfd, oob_data, strlen( oob_data ), MSG_OOB );
         send( sockfd, normal_data, strlen( normal_data ), 0 );
     }
 ​
     close( sockfd );
     return 0;
 }
 ​

接收代码:

arduino 复制代码
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <assert.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <string.h>
 ​
 #define BUF_SIZE 1024
 ​
 int main( int argc, char* argv[] )
 {
     if( argc <= 2 )
     {
         printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
         return 1;
     }
     const char* ip = argv[1];
     int port = atoi( argv[2] );
 ​
     struct sockaddr_in address;
     bzero( &address, sizeof( address ) );
     address.sin_family = AF_INET;
     inet_pton( AF_INET, ip, &address.sin_addr );
     address.sin_port = htons( port );
 ​
     int sock = socket( PF_INET, SOCK_STREAM, 0 );
     assert( sock >= 0 );
 ​
     int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
     assert( ret != -1 );
 ​
     ret = listen( sock, 5 );
     assert( ret != -1 );
 ​
     struct sockaddr_in client;
     socklen_t client_addrlength = sizeof( client );
     int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
     if ( connfd < 0 )
     {
         printf( "errno is: %d\n", errno );
     }
     else
     {
         char buffer[ BUF_SIZE ];
 ​
         memset( buffer, '\0', BUF_SIZE );
         ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
         printf( "got %d bytes of normal data '%s'\n", ret, buffer );
 ​
         memset( buffer, '\0', BUF_SIZE );
         ret = recv( connfd, buffer, BUF_SIZE-1, MSG_OOB );
         printf( "got %d bytes of oob data '%s'\n", ret, buffer );
 ​
         memset( buffer, '\0', BUF_SIZE );
         ret = recv( connfd, buffer, BUF_SIZE-1, 0 );
         printf( "got %d bytes of normal data '%s'\n", ret, buffer );
 ​
         close( connfd );
     }
 ​
     close( sock );
     return 0;
 }

实验结果:

发送数据

接收数据

Tcpdump抓取的数据包:

补充TCP的连接与分手

5.8.2 UDP数据读写

5.8.3 通用数据读写

5.11 socket选项

下面两个系统调用是专门用来读取和设置socket文件描述符属性的方法:

restrict关键字:告诉编译器该变量已经被指针修饰,不能通过其它直接或间接方式来修改它

c 复制代码
 #include <sys/socket.h>
 int getsockopt(int sockfd, int level, int option_name, void* option_value, 
     socklen_t* restrict option_len)
 int setsockopt(int sockfd, int level, int option_name, const void* option_value, 
     socklen_t option_len)

sockfd参数指定被操作的目标socket;

level参数指定要操作哪个协议的选项

option_name参数则指定选项的名字

option_value和option_len指定被操作选项的值和长度

因为有些设置是在TCP的同步报文时设置的。对于服务端而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效;对于客户端而言,有些socket选项应该在调用connect函数之前设置

5.11.1 SO_RCVBUF和SO_SNDBUF选项

以上两个选项分别表示接收缓冲区和发送缓冲区的大小。当我们用setsocket来设置TCP的接收缓冲区和发送缓冲区的大小的时,系统都会将其值加倍,并且不得小于某个最小值。 TCP接收缓冲区的最小值是256字节,发送缓冲区的最小值是2048字节(不同系统可能不一样)

修改TCP接收缓冲区的服务器程序

c 复制代码
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <assert.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <string.h>
 ​
 #define BUFFER_SIZE 1024
 ​
 int main( int argc, char* argv[] )
 {
     if( argc <= 3 )
     {
         printf( "usage: %s ip_address port_number receive_buffer_size\n", basename( argv[0] ) );
         return 1;
     }
     /*接收点分十进制字符串ip地址和端口号*/
     const char* ip = argv[1];
     int port = atoi( argv[2] );
 ​
     /*设置ipv4地址*/
     struct sockaddr_in address;
     bzero( &address, sizeof( address ) );
     address.sin_family = AF_INET;
     inet_pton( AF_INET, ip, &address.sin_addr );
     address.sin_port = htons( port );
 ​
     int sock = socket( PF_INET, SOCK_STREAM, 0 );
     assert( sock >= 0 );
     /*设置接收缓冲区大小*/
     int recvbuf = atoi( argv[3] );
     int len = sizeof( recvbuf );
     /*SOL_SOCKET表示通用socket选项,与协议无关*/
     printf( "the receive buffer size before settting is %d\n", recvbuf );
     setsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof( recvbuf ) );
     getsockopt( sock, SOL_SOCKET, SO_RCVBUF, &recvbuf, ( socklen_t* )&len );
     printf( "the receive buffer size after settting is %d\n", recvbuf );
 ​
     int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
     assert( ret != -1 );
 ​
     ret = listen( sock, 5 );
     assert( ret != -1 );
 ​
     struct sockaddr_in client;
     socklen_t client_addrlength = sizeof( client );
     int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
     if ( connfd < 0 )
     {
         printf( "errno is: %d\n", errno );
     }
     else
     {
         char buffer[ BUFFER_SIZE ];
         /*void *memset(void *buffer, int c, int count), c是被设置的值,count是缓冲区大小*/
         memset( buffer, '\0', BUFFER_SIZE );
         while( recv( connfd, buffer, BUFFER_SIZE-1, 0 ) > 0 ){}
         close( connfd );
     }
 ​
     close( sock );
     return 0;
 }

修改TCP发送缓冲区的客户端程序

c 复制代码
 #include <sys/socket.h>
 #include <arpa/inet.h>
 #include <assert.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <string.h>
 #include <stdlib.h>
 ​
 #define BUFFER_SIZE 512
 ​
 int main( int argc, char* argv[] )
 {
     if( argc <= 3 )
     {
         printf( "usage: %s ip_address port_number send_bufer_size\n", basename( argv[0] ) );
         return 1;
     }
     const char* ip = argv[1];
     int port = atoi( argv[2] );
 ​
     struct sockaddr_in server_address;
     bzero( &server_address, sizeof( server_address ) );
     server_address.sin_family = AF_INET;
     inet_pton( AF_INET, ip, &server_address.sin_addr );
     server_address.sin_port = htons( port );
 ​
     int sock = socket( PF_INET, SOCK_STREAM, 0 );
     assert( sock >= 0 );
 ​
     int sendbuf = atoi( argv[3] );
     int len = sizeof( sendbuf );
     printf( "the tcp send buffer size before setting is %d\n", sendbuf );
     setsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof( sendbuf ) );
     getsockopt( sock, SOL_SOCKET, SO_SNDBUF, &sendbuf, ( socklen_t* )&len );
     printf( "the tcp send buffer size after setting is %d\n", sendbuf );
 ​
     if ( connect( sock, ( struct sockaddr* )&server_address, sizeof( server_address ) ) != -1 )
     {
         char buffer[ BUFFER_SIZE ];
         memset( buffer, 'a', BUFFER_SIZE );
         send( sock, buffer, BUFFER_SIZE, 0 );
     }
 ​
     close( sock );
     return 0;
 }

服务器接收端

客户端发送端

Tcpdump抓包数据

5.11.2 SO_REVLOWAT和SO_SNDLOWAT选项

两选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读可写。当TCP接收缓冲区中可读数据的综述大于其低水位标记时,I/O复用系统将通知应用程序可以从其对应的socket上读取数据;当TCP发送缓冲区的空闲空间大于低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据

相关推荐
渣哥1 分钟前
从代理到切面:Spring AOP 的本质与应用场景解析
javascript·后端·面试
文心快码BaiduComate18 分钟前
文心快码3.5S实测插件开发,Architect模式令人惊艳
前端·后端·架构
5pace23 分钟前
【JavaWeb|第二篇】SpringBoot篇
java·spring boot·后端
HenryLin24 分钟前
Kronos核心概念解析
后端
oak隔壁找我24 分钟前
Spring AOP源码深度解析
java·后端
货拉拉技术26 分钟前
大规模 Kafka 消费集群调度方案
后端
oak隔壁找我27 分钟前
MyBatis Plus 源码深度解析
java·后端
oak隔壁找我27 分钟前
Druid 数据库连接池源码详细解析
java·数据库·后端
剽悍一小兔27 分钟前
Nginx 基本使用配置大全
后端
LCG元28 分钟前
性能排查必看!当Linux服务器CPU/内存飙高,如何快速定位并"干掉"罪魁祸首进程?
linux·后端