基于socket的Tcp多进程多线程服务器

时间:2021-02-04 16:42:37

套接字的概念

套接字(socket)这个词在Tcp/Ip协议中,“IP地址”+ “TCP或UDP端口号”唯一标识网络通信中的一个进程,这个“ip地址”+“端口号”就称为socket.

预备知识

1.在实现我们的服务器之前我们需要 了解一些预备知识。
我们都知道主机有大端和小端之分,那么内存之中存储的数据也就有了大端和小端之分,而通讯双方的主机之间发送数据时因为大端和小端的缘故,发送和接受都需对数据的存储做相应的修改,非常不方便,为了使我们的网络程序有更好的移植性,我们调用以下的库函数做网络字节流和主机节序的转换。
基于socket的Tcp多进程多线程服务器
h 表示 host n 表示 network l表示32位长整形 s表示16位短整形

利用这些库函数做相应的大小端转换后返回。

2.socket地址的数据类型。
socket数据结构
基于socket的Tcp多进程多线程服务器

由图我们也可以看出来各种socket地址结构的开头是相同的,前16为表示结构体的长度,后16位表示地址类型,因此socket可以接受各种类型的socketaddr结构体指针做参数,但是这些函数的参数都用stuct socket *类型表示,使用前需要做强制转换。

3.相关函数

1)字符串转in_addr的函数:
对于IP地址,存放在结构体sockaddr_in中的成员struct in_addr. sin_addr 表示32位的IP地址。 而我们通常习惯性的用点分十进制的字符串表示IP地址。 所以需要将点分十进制的IP字符串转换为整形

基于socket的Tcp多进程多线程服务器

2)创建套接字(获得一个文件描述符)
对于server和client来说,都需要创建一个套接字,来标识自己主机的IP地址及端口号,一对套接字代表一个链接。
基于socket的Tcp多进程多线程服务器

第一个参数:IPV4协议 使用 AF_INET.
第二个参数:Tcp 协议 使用 SOCK_STREAM(面向字节流传输协议)
Udp 协议 使用 SOCK_DGRAM(面向数据报传输协议)
第三个参数:默认0
返回值:调用成功返回一个文件描述符,失败返回-1

3)Bind绑定主机和端口号
将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。

int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen)

注意:
这里的网络地址可以是INADDR_ANY (0),这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。

因为服务器程序所监听的网络地址和端口号一般通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接。 因此服务器需要调用bind()绑定一个固定的网络地址和端口号。
而由于客户端不需要固定的端口号,因此不需要绑定,客户端的端口号是由内核自动分配的。

4)listen 监听
基于socket的Tcp多进程多线程服务器

第二个参数:表示需要监听的个数。
注意的是客户端不需要监听,只有服务器才需要监听。
返回值:成功返回0, 失败返回-1.

5)accept 接受请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
第一个参数:套接字的文件描述符。
返回值:成功返回客户端的ip地址和端口号。
在客户端未连接上的时候,在tcp三次握手之后阻塞式等待。

6)connect 请求链接
基于socket的Tcp多进程多线程服务器
是客户端向服务器发起连接请求的函数,
第一个参数:套接字的文件描述符。
第二个参数:服务器端的ip地址和端口号。
成功返回0, 失败返回-1;

4.演示代码

(1)服务器端server.c

  1 #include <stdio.h>
2 #include <netinet/in.h>
3 #include <arpa/inet.h>
4 #include <sys/socket.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <errno.h>
8 #include <sys/types.h>
9
10 static void Usage(char* proc)
11 {
12 printf("%s[local_ip], [local_port]\n", proc);
13 }
14
15 int startup(const char* _ip, int _port)
16 {
17 int sock = socket(AF_INET, SOCK_STREAM, 0);
18 if(sock < 0)
19 {
20 perror("socket");
21 exit(5);
22 }
23 struct sockaddr_in local;//初始化协议地址
24 local.sin_family = AF_INET;
25 local.sin_port = htons(_port);
26 local.sin_addr.s_addr = inet_addr(_ip);
27
28 //将套接字和tcp服务绑定(服务端ip地址)
29 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
30 {
31 perror("bind");
32 exit(3);
33 }
34 //监听这个套接字,监听指定端口,第二个参数表示可以排队连接的最大个数
35 if(listen(sock, 5) < 0)
36 {
37 perror("listen");
38 exit(4);
39 }
40 return sock;
41 }
42
43 //argv[]指针数组,指向各个参数
44 int main(int argc, char* argv[])
45 {
46 if(argc != 3)
47 {
48 Usage(argv[0]);
49 return 1;
50 }
51
52 int listen_sock = startup(argv[1], atoi(argv[2]));
53 printf("sock:%d\n", listen_sock);
54 char buf[1024];
55 while(1)
56 {
57 //接受client套接字的信息
58 struct sockaddr_in client;
59 socklen_t len = sizeof(client);
60 //服务人员
61 int new_fd = accept(listen_sock, (struct sockaddr*)&client, &len);
62 if(new_fd < 0)
63 {
64 perror("accept");
65 continue;
66 }
67 //将网络中的数据转换为主机用户看得懂的数据
68 printf("get a new client %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
69
70 while(1)
71 {
72 //char buf[1025];
73 ssize_t s = read(new_fd, buf, sizeof(buf) - 1);
74 if(s > 0)
75 {
76 buf[s] = 0;
77 printf("client# %s\n", buf);
78 //服务器将读到的数据给客户端回显回去
79 write(new_fd, buf, strlen(buf));
80 }
81 else if(s == 0)
82 {
83 printf("client quit...\n");
84 break;
85 }
86 else
87 {
88 break;
89 }
90
91 }
92 close(new_fd);
93 }
94 close(listen_sock);
95 return 0;
96 }

这是一个只能连接一个客户端的服务器,我们知道在实际中会有很多客户端同时连接服务器,所以我们想到利用多进程来实现。

(2)多进程服务器端 server.c

  1 #include <stdio.h>
2 #include <netinet/in.h>
3 #include <sys/socket.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <errno.h>
7 #include <unistd.h>
8
9 static void Usage(char* proc)
10 {
11 printf("%s[local_ip], [local_port]\n", proc);
12 }
13
14 int startup(char* _ip, int _port)
15 {
16 int sock = socket(AF_INET, SOCK_STREAM, 0);
17 if(sock < 0)
18 {
19 perror("socket");
20 return 1;
21 }
22 struct sockaddr_in local;//初始化协议地址
23 local.sin_family = AF_INET;
24 local.sin_port = htons(_port);
25 local.sin_addr.s_addr = inet_addr(_ip);
26
27 //将套接字和tcp服务绑定(服务端ip地址)
28 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
29 {
30 perror("bind");
31 exit(3);
32 }
33 //监听这个套接字,监听指定端口,第二个参数表示可以排队连接的最大个数
34 if(listen(sock, 5) < 0)
35 {
36 perror("listen");
37 }
38 return sock;
39 }
40
41 //argv[]指针数组,指向各个参数
42 int main(int argc, char* argv[])
43 {
44 if(argc != 3)
45 {
46 Usage(argv[0]);
47 return 2;
48 }
49
50 int listen_sock = startup(argv[1], atoi(argv[2]));
51 printf("sock:%d\n", listen_sock);
52 //需要让子进程的子进程去提供服务
53 //父进程继续监听
54 char buf[1024];
55 while(1)
56 {
57 struct sockaddr_in client;
58 socklen_t len = sizeof(client);
59 int newsock = accept(listen_sock, (struct sockaddr*)&client, &len);
60 if(newsock < 0)
61 {
62 perror("accept");
63 continue;
64 }
65
66 //将网络中的数据转换为主机用户可以看懂的数据
67 printf("get a new client %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
68
69 pid_t id = fork();
70 if(id < 0)
71 {
72 perror("fork");
73 close(newsock);
74 return 1;
75 }
76 else if(id == 0)
77 {
78 //因为子进程会继承父进程的文件描述符表,而子进程只需要newsock
79 close(listen_sock);//子进程关闭监听套接字
80 if(id > 0)
81 {
82 exit(0);//子进程充当父进程角色,父进程推出后,会导致子进程成为孤儿进程
83 }
84 //让子进程的子进程去读和写
85 while(1)
86 {
87 int s = read(newsock, buf, sizeof(buf) - 1);
88 if(s > 0)
89 {
90 buf[s] = 0;
91 printf("client# %s\n", buf);
92 write(newsock, buf, strlen(buf));
93 }
94 else if(s == 0)
95 {
96 printf("client quit\n");
97 }
98 else
99 {
100 break;
101 }
102 }
103 close(newsock);
104 exit(1);
105 }
106 else
107 {
108 //父进程,只负责监听
109 close(newsock);
110 waitpid(id, NULL, 0);
111 }
112 }
113 close(listen_sock);
114 return 0;
115 }

可能有人会问既然进程可以,那么线程能?而且线程比进程有更大的优势。答案是肯定可以的。

(3)多线程服务器端 server.c

  1 #include <stdio.h>
2 #include <netinet/in.h>
3 #include <sys/socket.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <errno.h>
7 #include <unistd.h>
8 #include <pthread.h>
9
10 static void Usage(char* proc)
11 {
12 printf("%s[local_ip], [local_port]\n", proc);
13 }
14
15 int startup(char* _ip, int _port)
16 {
17 int sock = socket(AF_INET, SOCK_STREAM, 0);
18 if(sock < 0)
19 {
20 perror("socket");
21 return 1;
22 }
23 struct sockaddr_in local;//初始化协议地址
24 local.sin_family = AF_INET;
25 local.sin_port = htons(_port);
26 local.sin_addr.s_addr = inet_addr(_ip);
27
28 //将套接字和tcp服务绑定(服务端ip地址)
29 if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
30 {
31 perror("bind");
32 exit(3);
33 }
34 //监听这个套接字,监听指定端口,第二个参数表示可以排队连接的最大个数
35 if(listen(sock, 5) < 0)
36 {
37 perror("listen");
38 }
39 return sock;
40 }
41
42 void* handle(void* argc)
43 {
44 int newsock = (int)argc;
45 char buf[1024];
46 while(1)
47 {
48 int s = read(newsock, buf, sizeof(buf) - 1);
49 if(s > 0)
50 {
51 buf[s] = 0;
52 printf("client# %s\n", buf);
53 write(newsock, buf, strlen(buf));//服务器回显
54 }
55 else if(s == 0)
56 {
57 printf("client quit\n");
58 }
59 else
60 {
61 break;
62 }
63 }
64 close(newsock);
65 }
66
67
68 //argv[]指针数组,指向各个参数
69 int main(int argc, char* argv[])
70 {
71 if(argc != 3)
72 {
73 Usage(argv[0]);
74 return 2;
75 }
76
77 int listen_sock = startup(argv[1], atoi(argv[2]));
78 printf("sock:%d\n", listen_sock);
79 //需要让子进程的子进程去提供服务
80 //父进程继续监听
81 char buf[1024];
82 while(1)
83 {
84 struct sockaddr_in client;
85 socklen_t len = sizeof(client);
86 int newsock = accept(listen_sock, (struct sockaddr*)&client, &len);
87 if(newsock < 0)
88 {
89 perror("accept");
90 continue;
91 }
92
93 //将网络中的数据转换为主机用户可以看懂的数据
94 printf("get a new client %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
95
96 //创建一个新的线程去服务
97 //主线程只负责监听工作
98 pthread_t tid;
99 pthread_create(&tid, NULL, handle, (void*)newsock);
100 pthread_detach(tid);
101 }
102 close(listen_sock);
103 return 0;
104 }

客户端 client.c

  1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <arpa/inet.h>
5 #include <stdlib.h>
6 #include <string.h>
7
8 static void usage(const char* proc)
9 {
10 printf("%s[server_ip][server_port]\n", proc);
11 }
12
13 //./client server_ip, server_port
14 int main(int argc, char* argv[])
15 {
16 if(argc != 3)
17 {
18 usage(argv[0]);
19 return 1;
20 }
21
22 //1.创建sock
23 int sock = socket(AF_INET, SOCK_STREAM, 0);
24 if(sock < 0)
25 {
26 perror("socket");
27 return 2;
28 }
29
30 //2.connect
31 struct sockaddr_in server;
32 server.sin_family = AF_INET;
33 server.sin_port = htons(atoi(argv[2]));
34 //将点分十进制的字符串转换成能在网络上传输的网络号
35 server.sin_addr.s_addr = inet_addr(argv[1]);
36
37 //调用connect,第一个参数是客户端的socket套接字,第二个参数是服务器端套接字
38 if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
39 {
40 perror("connect");
41 return 2;
42 }
43
44 //先写后读
45 char buf[1024];
46 while(1)
47 {
48 printf("Please enter# ");
49 fflush(stdout);
50 //读取标准输入键盘中的数据
51 ssize_t s = read(0, buf, sizeof(buf) - 1);
52 if(s > 0)
53 {
54 buf[s - 1] = 0;
55 //将buf中的内容写到套接字中
56 write(sock, buf, strlen(buf));
57 //读取服务器的响应
58 ssize_t _s = read(sock, buf, sizeof(buf)-1);
59 if(_s > 0)
60 {
61 buf[_s] = 0;
62 printf("server ech0# %s\n", buf);
63 }
64 }
65 }
66 close(sock);
67 return 0;
68 }

运行结果:
基于socket的Tcp多进程多线程服务器
我们用多线程服务器端测试,使用了两个客户端同时连上服务器端。

这样我们可能没有发现问题,但是确实存在的一个bug是,当服务器端退出后立即启动时会错误显示地址正在被使用,那这个在实际中肯定是很常见的,所以我们需要处理这个bug.

TCP协议规定:
主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL的时间后才能回到CLOSED状态。因为主动断开的一方数据传输完成,而被动一方可能还有数据要传输,所以需要主动断开的一方处于TIME_WAIT状态。

在上边的代码中我们先终止了server,所以server是主动关闭连接的一方,所以server就处于TIME_WAIT状态。所以在TIME_WAIT期间仍然不能再次监听同样的server端口。

MSL在RFC1122中规定为两分钟,但是各种不同操作系统的实现不同,在Linux上一般经过半分钟后,就可以再次启动server了。

解决方案:
在server的TCP连接没有完全断开之前不允许重新监听是不合理的,因为TCP连接没有完全断开指的是newsock没有完全断开,而我们重新监听的是listen_sock,虽然是占用同一个端口,但是IP地址不同,newsock对应的是与某个客户端通讯的一个具体的IP地址。而listen_scok对应的是某个匹配的IP地址。

解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同而IP地址不同的多个socket描述符。
在server代码的socket()和bind()调用之间插入如下代码:

int opt = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

完整代码见:
https://github.com/blight-888/CentOs-/tree/master/server/tcp_server