五十五、linux 编程——TCP 连接和关闭过程及服务器的并发处理
55.1 TCP 连接和关闭过程
55.1.1 介绍
建立连接的过程就是三次握手的过程:客户端发送 SYN 报文给服务器,服务器回复 SYN+ACK 报文,客户机再发送 ACK 报文。
关闭连接的过程:客户机先发送 FIN 报文,服务器回复 ACK 报文,服务器再发送 FIN 报文,客户机再发送响应报文 ACK。
55.1.2 自定义协议编程例子
msg.h
1 #ifndef __MSG_H__ 2 #define __MSG_H__ 3 4 #include <sys/types.h> 5 6 typedef struct { 7 /** 协议头部: 不传输任何数据,只包含发送端的一些信息 */ 8 char head[10]; ///< 协议头部 9 char checknum; ///< 校验码 10 11 /**协议体部 */ 12 char buff[512]; ///< 数据 13 }Msg; 14 15 /** 发送一个基于自定义协议的 message,发送的数据存放在 buff 中 */ 16 extern int write_msg(int sockfd, char *buff, ssize_t len); 17 18 /** 读取一个基于自定义协议的 message, 读取的数据存放在 buff 中 */ 19 extern int read_msg(int sockfd, char *buff, ssize_t len); 20 21 #endif
msg.c
1 #include "msg.h" 2 #include <unistd.h> 3 #include <string.h> 4 #include <memory.h> 5 #include <sys/types.h> 6 7 8 /** 计算校验码 */ 9 static unsigned char msg_check(Msg *message) 10 { 11 unsigned char s = 0; 12 int i; 13 for(i = 0; i < sizeof(message->head); i++){ 14 s += message->head[i]; 15 } 16 17 for(i = 0; i < sizeof(message->buff); i++){ 18 s += message->buff[i]; 19 } 20 21 return s; 22 } 23 24 25 /** 发送一个基于自定义协议的 message,发送的数据存放在 buff 中 */ 26 int write_msg(int sockfd, char *buff, ssize_t len) 27 { 28 Msg message; 29 memset(&message, 0, sizeof(message)); 30 strcpy(message.head, "hello"); 31 memcpy(message.buff, buff, len); 32 message.checknum = msg_check(&message); 33 34 if(write(sockfd, &message, sizeof(message)) != sizeof(message)){ 35 return -1; 36 } 37 38 return 0; 39 } 40 41 /** 读取一个基于自定义协议的 message, 读取的数据存放在 buff 中 */ 42 int read_msg(int sockfd, char *buff, ssize_t len) 43 { 44 Msg message; 45 memset(&message, 0, sizeof(message)); 46 47 ssize_t size; 48 if((size = read(sockfd, &message, sizeof(message))) < 0){ 49 return -1; 50 } 51 else if(size == 0){ 52 return 0; 53 } 54 55 /** 进行校验码验证,判断接收到的 message 是否完整 */ 56 unsigned char s = msg_check(&message); 57 if((s == (unsigned char)message.checknum) && (!strcmp("hello", message.head))){ 58 memcpy(buff, message.buff, len); 59 return sizeof(message); 60 } 61 return -1; 62 63 }
编译成 .o 文件:gcc -o obj/msg.o -Iinclude -c src/msg.c
55.2 服务器的并发过程
55.2.1 介绍
一个服务器处理多个客户端的请求,就称为服务器的并发。
- 服务器端并发性处理
- 多进程模型
- 多线程模型
- I/O多路转换(select)
55.2.2 基于自定义协议的多进程模型编程
(1)服务器代码
echo_tcp_server.c
1 #include <netdb.h> 2 #include <netinet/in.h> 3 #include <sys/socket.h> 4 #include <sys/wait.h> 5 #include <unistd.h> 6 #include <string.h> 7 #include <stdio.h> 8 #include <stdlib.h> 9 #include <memory.h> 10 #include <signal.h> 11 #include <time.h> 12 #include <arpa/inet.h> 13 #include <errno.h> 14 #include "msg.h" 15 16 17 int sockfd; 18 19 void sig_handler(int signo) 20 { 21 if(signo == SIGINT){ 22 printf("server close\n"); 23 /** 步骤6: 关闭 socket */ 24 close(sockfd); 25 exit(1); 26 } 27 28 if(signo == SIGINT){ 29 printf("child process deaded...\n"); 30 wait(0); 31 } 32 } 33 34 /** 输出连接上来的客户端相关信息 */ 35 void out_addr(struct sockaddr_in *clientaddr) 36 { 37 /** 将端口从网络字节序转换成主机字节序 */ 38 int port = ntohs(clientaddr->sin_port); 39 char ip[16]; 40 memset(ip, 0, sizeof(ip)); 41 /** 将 ip 地址从网络字节序转换成点分十进制 */ 42 inet_ntop(AF_INET, &clientaddr->sin_addr.s_addr, ip, sizeof(ip)); 43 printf("client: %s(%d) connected\n", ip, port); 44 } 45 46 void do_service(int fd) 47 { 48 /** 和客户端进行读写操作(双向通信) */ 49 char buff[512]; 50 while(1){ 51 memset(buff, 0, sizeof(buff)); 52 printf("start read and write....\n"); 53 ssize_t size; 54 if((size = read_msg(fd, buff, sizeof(buff))) < 0){ 55 perror("protocal error"); 56 break; 57 } 58 else if(size == 0){ 59 break; 60 } 61 else { 62 printf("%s\n", buff); 63 if(write_msg(fd, buff, sizeof(buff)) < 0){ 64 if(errno == EPIPE){ 65 break; 66 } 67 perror("protocal error"); 68 } 69 } 70 } 71 } 72 73 int main(int argc, char *argv[]) 74 { 75 if(argc < 2){ 76 printf("usage: %s #port\n", argv[0]); 77 exit(1); 78 } 79 80 if(signal(SIGINT, sig_handler) == SIG_ERR){ 81 perror("signal sigint error"); 82 exit(1); 83 } 84 85 if(signal(SIGCHLD, sig_handler) == SIG_ERR){ 86 perror("signal sigchld error"); 87 exit(1); 88 } 89 90 /** 步骤1: 创建 socket(套接字) 91 * 注: socket 创建在内核中,是一个结构体. 92 * AF_INET: IPV4 93 * SOCK_STREAM: tcp 协议 94 * AF_INET6: IPV6 95 */ 96 sockfd = socket(AF_INET, SOCK_STREAM, 0); 97 if(sockfd < 0){ 98 perror("socket error"); 99 exit(1); 100 } 101 102 /** 103 * 步骤2: 调用 bind 函数将 socket 和地址(包括 ip、port)进行绑定 104 */ 105 struct sockaddr_in serveraddr; 106 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 107 /** 往地址中填入 ip、port、internet 地址族类型 */ 108 serveraddr.sin_family = AF_INET; ///< IPV4 109 serveraddr.sin_port = htons(atoi(argv[1])); ///< 填入端口 110 serveraddr.sin_addr.s_addr = INADDR_ANY; ///< 填入 IP 地址 111 if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in))){ 112 perror("bind error"); 113 exit(1); 114 } 115 116 /** 117 * 步骤3: 调用 listen 函数启动监听(指定 port 监听) 118 * 通知系统去接受来自客户端的连接请求 119 * (将接受到的客户端连接请求放置到对应的队列中) 120 * 第二个参数: 指定队列的长度 121 */ 122 if(listen(sockfd, 10) < 0){ 123 perror("listen error"); 124 exit(1); 125 } 126 127 /** 128 * 步骤4: 调用 accept 函数从队列中获得一个客户端的请求连接, 并返回新的 129 * socket 描述符 130 * 注意: 若没有客户端连接,调用此函数后会足则, 直到获得一个客户端的连接 131 */ 132 struct sockaddr_in clientaddr; 133 socklen_t clientaddr_len = sizeof(clientaddr); 134 while(1){ 135 int fd = accept(sockfd, (struct sockaddr *)&clientaddr, &clientaddr_len); 136 if(fd < 0){ 137 perror("accept error"); 138 continue; 139 } 140 141 /** 142 * 步骤5: 启动子进程去调用 IO 函数(read/write)和连接的客户端进行双向的通信 143 */ 144 pid_t pid = fork(); 145 if(pid < 0){ 146 continue; 147 } 148 else if(pid == 0){ 149 /** 子进程 */ 150 out_addr(&clientaddr); 151 do_service(fd); 152 /** 步骤6: 关闭 socket */ 153 close(fd); 154 break; 155 } 156 else{ 157 /** 父进程 */ 158 /** 步骤6: 关闭 socket */ 159 close(fd); 160 } 161 } 162 163 return 0; 164 }
gcc -o bin/echo_tcp_server -Iinclude obj/msg.o src/echo_tcp_server.c
(2)客户端代码
echo_tcp_client.c
1 #include <sys/types.h> 2 #include <stdlib.h> 3 #include <stdio.h> 4 #include <memory.h> 5 #include <unistd.h> 6 #include <sys/socket.h> 7 #include <netdb.h> 8 #include <signal.h> 9 #include <string.h> 10 #include <time.h> 11 #include <arpa/inet.h> 12 #include "msg.h" 13 14 15 int main(int argc, char *argv[]) 16 { 17 if(argc < 3){ 18 printf("usage: %s ip port\n", argv[0]); 19 exit(1); 20 } 21 22 /** 步骤1: 创建 socket */ 23 int sockfd = socket(AF_INET, SOCK_STREAM, 0); 24 if(sockfd < 0){ 25 perror("socket error"); 26 exit(1); 27 } 28 29 /** 往 serveraddr 中填入 ip、port 和地址族类型(ipv4) */ 30 struct sockaddr_in serveraddr; 31 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 32 serveraddr.sin_family = AF_INET; 33 serveraddr.sin_port = htons(atoi(argv[2])); 34 /** 将 ip 地址转换成网络字节序后填入 serveraddr 中 */ 35 inet_pton(AF_INET, argv[1], &serveraddr.sin_addr.s_addr); 36 37 /** 38 * 步骤2: 客户端调用 connect 函数连接到服务器端 39 */ 40 if(connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in)) < 0){ 41 perror("connect error"); 42 exit(1); 43 } 44 45 /** 步骤3: 调用 IO 函数(read/write)和服务器端进行双向通信 */ 46 char buff[512]; 47 ssize_t size; 48 char *prompt = "==>"; 49 while(1){ 50 memset(buff, 0, sizeof(buff)); 51 write(STDOUT_FILENO, prompt, 3); 52 size = read(STDIN_FILENO, buff, sizeof(buff)); 53 if(size < 0) continue; 54 buff[size - 1] = '\0'; 55 56 if(write_msg(sockfd, buff, sizeof(buff)) < 0){ 57 perror("write msg error"); 58 continue; 59 } 60 else { 61 if(read_msg(sockfd, buff, sizeof(buff)) < 0){ 62 perror("read msg error"); 63 continue; 64 } 65 else { 66 printf("%s\n", buff); 67 } 68 } 69 } 70 71 /** 步骤4: 关闭 socket */ 72 close(sockfd); 73 74 return 0; 75 }
gcc -o bin/echo_tcp_client -Iinclude obj/msg.o src/echo_tcp_client.c
(3)测试
开启两个终端进行测试,一个运行服务器,一个运行客户端:
55.2.3 基于自定义协议的多线程模型编程
echo_tcp_server_th.c
1 #include <netdb.h> 2 #include <netinet/in.h> 3 #include <sys/socket.h> 4 #include <sys/wait.h> 5 #include <unistd.h> 6 #include <string.h> 7 #include <stdio.h> 8 #include <stdlib.h> 9 #include <memory.h> 10 #include <signal.h> 11 #include <time.h> 12 #include <arpa/inet.h> 13 #include <errno.h> 14 #include "msg.h" 15 #include <pthread.h> 16 17 18 int sockfd; 19 20 void sig_handler(int signo) 21 { 22 if(signo == SIGINT){ 23 printf("server close\n"); 24 /** 步骤6: 关闭 socket */ 25 close(sockfd); 26 exit(1); 27 } 28 } 29 30 void do_service(int fd) 31 { 32 /** 和客户端进行读写操作(双向通信) */ 33 char buff[512]; 34 while(1){ 35 memset(buff, 0, sizeof(buff)); 36 ssize_t size; 37 if((size = read_msg(fd, buff, sizeof(buff))) < 0){ 38 perror("protocal error"); 39 break; 40 } 41 else if(size == 0){ 42 break; 43 } 44 else { 45 printf("%s\n", buff); 46 if(write_msg(fd, buff, sizeof(buff)) < 0){ 47 if(errno == EPIPE){ 48 break; 49 } 50 perror("protocal error"); 51 } 52 } 53 } 54 } 55 56 57 void out_fd(int fd) 58 { 59 struct sockaddr_in addr; 60 socklen_t len = sizeof(addr); 61 /** 从 fd 中获得连接的客户端相关信息并放置到 sockaddr_in 当中 */ 62 if(getpeername(fd, (struct sockaddr *)&addr, &len) < 0){ 63 perror("getpeername error"); 64 return; 65 } 66 67 char ip[16]; 68 memset(ip, 0, sizeof(ip)); 69 int port = ntohs(addr.sin_port); 70 inet_ntop(AF_INET, &addr.sin_addr.s_addr, ip, sizeof(ip)); 71 printf("%16s(%5d) closed!\n", ip, port); 72 } 73 74 void *th_fn(void *arg) 75 { 76 int fd = (int)arg; 77 78 do_service(fd); 79 out_fd(fd); 80 close(fd); 81 return (void *)0; 82 } 83 84 int main(int argc, char *argv[]) 85 { 86 if(argc < 2){ 87 printf("usage: %s #port\n", argv[0]); 88 exit(1); 89 } 90 91 if(signal(SIGINT, sig_handler) == SIG_ERR){ 92 perror("signal sigint error"); 93 exit(1); 94 } 95 96 97 /** 步骤1: 创建 socket(套接字) 98 * 注: socket 创建在内核中,是一个结构体. 99 * AF_INET: IPV4 100 * SOCK_STREAM: tcp 协议 101 * AF_INET6: IPV6 102 */ 103 sockfd = socket(AF_INET, SOCK_STREAM, 0); 104 if(sockfd < 0){ 105 perror("socket error"); 106 exit(1); 107 } 108 109 /** 110 * 步骤2: 调用 bind 函数将 socket 和地址(包括 ip、port)进行绑定 111 */ 112 struct sockaddr_in serveraddr; 113 memset(&serveraddr, 0, sizeof(struct sockaddr_in)); 114 /** 往地址中填入 ip、port、internet 地址族类型 */ 115 serveraddr.sin_family = AF_INET; ///< IPV4 116 serveraddr.sin_port = htons(atoi(argv[1])); ///< 填入端口 117 serveraddr.sin_addr.s_addr = INADDR_ANY; ///< 填入 IP 地址 118 if(bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr_in))){ 119 perror("bind error"); 120 exit(1); 121 } 122 123 /** 124 * 步骤3: 调用 listen 函数启动监听(指定 port 监听) 125 * 通知系统去接受来自客户端的连接请求 126 * (将接受到的客户端连接请求放置到对应的队列中) 127 * 第二个参数: 指定队列的长度 128 */ 129 if(listen(sockfd, 10) < 0){ 130 perror("listen error"); 131 exit(1); 132 } 133 134 /** 135 * 步骤4: 调用 accept 函数从队列中获得一个客户端的请求连接, 并返回新的 136 * socket 描述符 137 * 注意: 若没有客户端连接,调用此函数后会足则, 直到获得一个客户端的连接 138 */ 139 140 /** 设置线程的分离属性 */ 141 pthread_attr_t attr; 142 pthread_attr_init(&attr); 143 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 144 145 while(1){ 146 /** 主控线程负责调用 accept 去获得客户端的连接 */ 147 int fd = accept(sockfd, NULL, NULL); 148 if(fd < 0){ 149 perror("accept error"); 150 continue; 151 } 152 153 /** 154 * 步骤5: 启动子线程去调用 IO 函数(read/write)和连接的客户端进行双向的通信 155 */ 156 pthread_t th; 157 int err; 158 /** 以分离状态启动子线程 */ 159 if((err = pthread_create(&th, &attr, th_fn, (void *)fd)) != 0){ 160 perror("pthread create error"); 161 } 162 163 pthread_attr_destroy(&attr); 164 165 } 166 167 return 0; 168 }
客户端程序用上面的客户端程序即可。