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 }
客户端程序用上面的客户端程序即可。