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上写入数据

相关推荐
杨哥带你写代码27 分钟前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
A尘埃1 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23071 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code1 小时前
(Django)初步使用
后端·python·django
代码之光_19801 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长1 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记2 小时前
DataX+Crontab实现多任务顺序定时同步
后端
姜学迁3 小时前
Rust-枚举
开发语言·后端·rust
爱学习的小健4 小时前
MQTT--Java整合EMQX
后端
北极小狐4 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端