前面我对TCP进行了单对单的讲解,使用连接就回复"Hello, world"的例子说明简单tcp的连接传输,但一般来说,我们经常见到的服务器是属于那种单对多的,一台服务器可以为多个客户提供服务的,那这个怎么实现的呢?所以这里针对这个单对多进行一下赘述。
针对实现,我们知道前面的例子是服务端处理完单个客户端的连接请求就退出了服务,所以我们想要实现单个服务器对应多个客户请求,就可以循环调用accept函数,使得它呈现以下的流程:
然后我们实现一下回声服务的迭代服务
//echo_server.c
#include
#include
#include
#include
#include
#include
#define BUF 1024
void errorHandling(char* message);
int main(int argc, char* argv[]) {
int serv_sock, clnt_sock;
char message[BUF];
int str_len, i = 0;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_size;
if (argc != 2) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
//创建套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
errorHandling("socket() error!");
//地址信息构建
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//注册服务端套接字和服务端地址
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("bind() error!");
if (listen(serv_sock, 5) == -1)
errorHandling("listen() error!");
clnt_addr_size = sizeof(clnt_addr);
while (i < 5) {
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
errorHandling("accept() error!");
else
printf("第%d客户端 : %s 连接上了\n", ++i, inet_ntoa(clnt_addr.sin_addr));
//当前客户端回声服务
while((str_len = read(clnt_sock, message, BUF)) != 0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
//错误信息提示
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
//echo_client.c
#include
#include
#include
#include
#include
#include
#define BUF 1024
void errorHandling(char* message);
int main(int argc, char* argv[]){
int sock, str_len;
char message[BUF];
struct sockaddr_in serv_addr;
if (argc != 3) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("connect() error!");
else
printf("Connected successfully......\n");
while(1) {
fputs("Me(q to quit): ", stdout);
fgets(message, BUF, stdin);
if (!strcmp(message, "q\n")||!strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
str_len = read(sock, message, BUF - 1);
message[str_len] = 0;
printf("Back: %s", message);
}
close(sock);
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
然后我们可以打开多个终端来运行我们的echo客户端,可以看到的是,多个客户端都会出现以下情形:
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./echo_client 127.0.0.1 9999
Connected successfully......
Me(q to quit):
蛮有意思的是,服务端那边并不一定给你进行连接的显示,也就是,客户端使用connect函数是成功的,但服务端那边并没有打印连接上的服务端信息。整个流程走完的服务端是这样显示的:
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./echo_server 9999
第1客户端 : 127.0.0.1 连接上了
第2客户端 : 127.0.0.1 连接上了
第3客户端 : 172.25.106.148 连接上了
第4客户端 : 127.0.0.1 连接上了
第5客户端 : 127.0.0.1 连接上了
为什么呢?因为它阻塞了,服务端根据程序走向,卡在了我们回应其中一个客户端的while循环中了,因为我们有两个while循环,外层循环接受客户端连接,内层循环利用和客户端连接的临时套接字和客户端进行通信。也就是说,同一时间,服务端只对一个客户端进行服务。但这样不行啊,我们需要的是同一时间能有多个客户端都有响应服务才行。这怎么做到呢?这就涉及并发了。
更新下回声服务的windows实现
//echo_server_win.c
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
#define BUF 30
#pragma comment(lib, "ws2_32.lib")
void errorHandling(char* message);
int main(int argc, char* argv[]) {
WSADATA wsa_data;
SOCKET serv_sock, clnt_sock;
SOCKADDR_IN serv_addr, clnt_addr;
char message[BUF];
int str_len, addr_size, i = 0;
if (argc != 2) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == INVALID_SOCKET)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (SOCKADDR*) &serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("bind() error!");
if (listen(serv_sock, 5) == SOCKET_ERROR)
errorHandling("listen() error!");
addr_size = sizeof(clnt_addr);
while (i < 5) {
clnt_sock = accept(serv_sock, (SOCKADDR*) &clnt_addr, &addr_size);
if (clnt_sock == INVALID_SOCKET)
errorHandling("accept() error!");
else
printf("第%d已连接客户端%s",++i ,inet_ntoa(clnt_addr.sin_addr));
while ((str_len = recv(clnt_sock, message, BUF, 0)) != 0)
send(clnt_sock, message, str_len, 0);
closesocket(clnt_sock);
}
closesocket(serv_sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
//echo_client_win.c
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include
#include
#include
#include
#define BUF 30
#pragma comment(lib, "ws2_32.lib")
void errorHandling(char* message);
int main(int argc, char* argv[]) {
WSADATA wsa_data;
SOCKET sock;
SOCKADDR_IN serv_addr;
char message[BUF];
int str_len, addr_size, i = 0;
if (argc != 3) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("connect() error!");
else
printf("Connected successfully.\n");
while (1) {
fputs("Me(q to quit):", stdout);
fgets(message, BUF, stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
send(sock, message, strlen(message), 0);
str_len = recv(sock, message, BUF - 1, 0);
message[str_len] = 0;
printf("Back: %s", message);
}
printf("%d 's connection've disconnected.\n", sock);
closesocket(sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
上面的实现添加了一个现实socket套接字的值的输出,大家做测试可以自己看看创建得到的套接字的值是多少,这个是有一定规律的,不过这个是后话。
关于并发,指的是逻辑控制流在时间上重叠,经常出现在内核运行程序上,而我们的程序开发也可以使用这个概念,尤其是在网络应用上。编写并发应用最直接的就是开进程开线程,不同进程/线程负责不同任务,windows中常常是开线程,linux中则开进程,这是因为在windows中新开一个线程的花销比进程要小,而linux中则是反过来,所以就出现这种状况。然后,我们知道各种服务器基本用的都是linux系统,所以下面内容就是linux下多进程实现并发服务器。
准备知识
前面我们可以知道,服务端有两个套接字,一个负责监听连接请求,一个负责连接通信,那我们现在就是在父进程负责监听连接,然后创建子进程来进行连接通信,这样就可以应对多个客户端的同时请求了。
对于进程,我们需要知道的就是它是运行中的程序,当它占用CPU在执行指令的时候就是处于执行状态,当它需要输入输出或者进行其他任务而暂停时,这个状态称为阻塞状态,还有就是进程所需资源都准备好了,就差CPU调用就是就绪状态,这个状态的进程处于一个调用队列中,一大堆进程都在这里等待CPU调用。
使用函数:
#include
pid_t fork(void); //成功则返回进程ID,失败返回-1
上面的函数专门用来创建子进程,所以在父进程中会返回进程ID,在子进程中则会返回0。父子进程共享同一份代码,但变量不共享。需要重视的是,资源的申请在c/c++中,往往需要自己回收销毁,而不能自动回收(虽然智能指针可以做到这点),所以在编写创建资源代码的同时就该考虑回收代码的编写。
子进程的使用也要注意,当子进程没有被正确销毁时,它就会成为僵尸进程卡着你的工作。为了防止产生僵尸进程,需要向创建子进程的父进程传递子进程的exit参数或return返回值,一般需要父进程主动获取子进程的结束状态值,实现这个功能的有以下两个函数:
#include
pid_t wait(int* statloc);
//成功会返回终止了的子进程ID,失败则返回-1
statloc指针所指位置可以用来保存子进程终止时传递的返回值,但这些信息不是单一的,需要分离:
WIFEXITITED(statloc);//子进程正常终止返回true,否则为false
WEXITSTATUS(statloc);//子进程返回值
调用wait函数,如果没有已终止的子进程,程序将会一直阻塞,这就是函数名wait的由来,你没结束,我等你。那如果不想阻塞呢?那就可以使用waitpid函数。
#include
pid_t waitpid(pid_t pid, int* statloc, int options);
//成功返回终止的子进程ID或0,失败返回-1
和上面wait函数不同的是,waitpid函数多出了pid和options参数,前者是等待终止的子进程ID,后者是条件选项,比如你传递WNOHANG常量作为options参数,程序就不会进入阻塞一直等子进程终止,而是返回0并退出函数。
信号通信
识别进程是否终结的工作是由操作系统来进行,所以不使用上面函数,我们可以由操作系统作为监控,在子进程终止时发送信息通知父进程。这就是信号处理机制。
信号也是一种资源,需要注册,以下的signal函数就实现注册信号功能并且在该信号产生时执行传参进去的函数指针指示函数。如下:
#include
void (*signal(int signo, void (*func)(int)))(int);
signo参数表示特殊情况,第二个参数为一个函数指针,用于此特殊情况,就是特殊情况发生时,调用func函数指针对应函数。signo参数对应的特殊情况有如下常数进行规定:
SIGALRM:到通过调用alarm函数注册的时候了
SIGINT:输入CTRL + C
SIGCHLD:子进程终止
这里使用的情况可以是这样:
signal(SIGCHLD, myHandler);
以上调用就表明子进程终止时调用myHandler函数
sigaction的使用
但这里要使用的并不是signal函数,而是更通用的sigaction函数,因为前者在UNIX不同操作系统中有不同,但后者在UNIX中完全相同。
#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
//成功返回0,失败返回-1
struct sigaction {
void (*sa_handler)(int); //保存信号处理函数指针
sigset_t sa_mask;
int sa_flags;
};
这里面出现了新的参数,sigaction结构体,函数中act参数是对应参数1的信号处理函数信息,oldact参数则是可以用来获取之前注册的信号处理函数指针,不需要可以传递0值。关于sigaction结构体的使用可以参考下面例子。
sigaction的使用
针对前面的回声客户端进行更改,添加父子进程流程和信号处理函数即可,如下:
#include
#include
#include
#include
#include
#include
#define BUF 30
//addition
#include
#include
void readChildProc(int sig);
void errorHandling(char* message);
int main(int argc, char *argv[]) {
//addition
pid_t pid;
struct sigaction act;
int state;
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t addr_size;
int str_len, i = 0;
char buf[BUF];
if (argc != 2) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
//addition
//设置act中信号处理函数
act.sa_handler = readChildProc;
//初始化sa_mask和sa_flags中所有位为0
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//调用sigaction函数
state = sigaction(SIGCHLD, &act, 0);
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("bind() error!");
if (listen(serv_sock, 5) == -1)
errorHandling("listen() error!");
while(1) {
addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &addr_size);
if (clnt_sock == -1)
continue;
else
printf("第%d个客户端连接上了,IP:%s", ++i, inet_ntoa(clnt_addr.sin_addr));
pid = fork();
if (pid == -1) {
//fork失败
close(clnt_sock);
continue;
} else if (pid == 0) {
//子进程运行部分
close(serv_sock);
while((str_len = read(clnt_sock, buf, BUF)) != 0)
write(clnt_sock, buf, str_len);
printf("Client: %s prepare to disconnect.\n", inet_ntoa(clnt_addr.sin_addr));
close(clnt_sock);
//子进程退出
return 0;
} else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void errorHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
void readChildProc(int sig) {
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed process ID: %d", pid);
}
以上服务器的连接和断开连接的信息都在客户端断开连接的时候一起输出,让我有点不太懂,不确定是否都保留在缓存中。而且使用腾讯服务器运行回声客户端连接本机ubuntu子系统的回声服务端一如既往的connect() error。真让人懵逼。
理一理上面服务端的流程:
我们之前已经知道,服务端只有一个套接字用于监听客户端连接请求,然后另有套接字用于连接客户端并传输信息。然后从上面我们可以看到,服务端的父进程中套接字监听客户端请求,子进程中另有套接字负责连接客户端并传输信息,并且断开连接也是通过这个套接字。
注:当调用fork函数时,父子进程分别拥有一个前面clnt_sock对应的套接字,而在子进程区域有一个代码是close(serv_sock)是因为同样是复制给了子进程。
现在来理一理这个概念,fork函数复制了serv_sock和clnt_sock是复制了套接字文件描述符。socket套接字,它是一种资源,我们调用socket函数时,是创建了套接字资源,然后分配一个整数来称呼这个套接字,这个就是文件描述符。也就是说,资源就一种,但用来使用这个资源的文件描述符父子进程都有了,当它close以后,就是该进程断开了使用该资源的资格,具体我们可以参考指针概念,有一个变量,但指向该变量的指针有两个,被两个人拥有,然后他们都可以通过该指针来使用这个变量,文件描述符就类比这个指针。
那什么时候会销毁套接字资源?当然是所有文件描述符都终止,都被close才会被销毁。
其实很早之前就已经在云服务器上面进行测试,可是每次都以失败告终,这次终于有点眉目了,主要还是防火墙的问题。
虽然防火墙开启了9999端口对应TCP服务,但连接还是以connect error告终,在云服务器上运行客户端,我的主机WSL2运行客户端也是长时间连接,没有效果。最后关了防火墙后成功了。
#centos7腾讯服务器,firewall-cmd工具
systemctl stop firewalld
命令关闭服务即可,然后我暂时还是关闭防火墙进行的测试,而且采取云服务器运行服务端程序,我的windows下cmd和wsl2虚拟机运行客户端进行测试。
win10cmd运行客户端然后WSL2下ubuntu运行客户端连接的效果:
#linux云服务器
[root@VM-0-17-centos ~]# ./echo_server 9999
连接117.61.105.77
#win10cmd
PS C:\Users\samu\Desktop> ./echo_client_win 121.5.47.242 9999
Connected successfully.
Me(q to quit):pei呸呸呸
Back: pei呸呸呸
#WSL2下ubuntu子系统
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./echo_client 121.5.47.242 9999
Connected successfully......
Me(q to quit): woshinidaye
可以看到的是由于win10下运行的客户端已经连接上了,然后ubuntu下客户端的连接只是发起请求并没有失败,而是进入了连接队列中,所以这边输入信息并没有回应。因为客户端只有当连接成功才能进行信息传输,而请求进入服务端的连接请求队列并不算connect成功,我上面的Connected successfully信息可以忽略掉,哈哈哈。
#win10cmd
PS C:\Users\samu\Desktop> ./echo_client_win 121.5.47.242 9999
Connected successfully.
Me(q to quit):q
276 's connection've disconnected.
请按任意键继续. . .
PS C:\Users\samu\Desktop>
#WSL2下ubuntu子系统
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./echo_client 121.5.47.242 9999
Connected successfully......
Me(q to quit): woshinidaye
Back: woshinidaye
Me(q to quit):
然后我们退出一下win10下的客户端,ubuntu的就可以连接成功并得到服务端的回应了。
接下来进行上面刚实现的多进程服务器测试,由于主要是对服务端的更改,所以测试用客户端还是那个。
#云服务器
[root@VM-0-17-centos ~]# ./echo_mul_server 9999
#win10cmd
PS C:\Users\samu\Desktop> ./echo_client_win 121.5.47.242 9999
Connected successfully.
Me(q to quit):shiwo
Back: shiwo
Me(q to quit):
#WSL2下ubuntu子系统
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./echo_client 121.5.47.242 9999
Connected successfully......
Me(q to quit): nidaye
Back: nidaye
Me(q to quit):
上面的多进程服务器运行起来是这样的,然后由于是多进程,对应的输出连接的提示语句会有点乱序,我们断开连接来看一下:
[root@VM-0-17-centos ~]# ./echo_mul_server 9999
第1个客户端连接上了,IP:117.61.105.77第2个客户端连接上了,IP:117.61.105.77Client: 117.61.105.77 prepare to disconnect.
第1个客户端连接上了,IP:117.61.105.77Client: 117.61.105.77 prepare to disconnect.
当我们的客户端都断开了连接,服务端的输出就会变成这样,主要还是由于多进程都输出在一起,没有整个格式,就像两个小孩在一幅画版上面画画,你画你的,我画我的,不是协作式的作业。
除此以外,我们怎么知道它真的开了进程内?可以后台查看一下使用情况的
[root@VM-0-17-centos ~]# netstat -lnpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:9009 0.0.0.0:* LISTEN 1669/clickhouse-ser
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 7531/sshd
tcp 0 0 127.0.0.1:8123 0.0.0.0:* LISTEN 1669/clickhouse-ser
tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN 1145/smbd
tcp 0 0 0.0.0.0:9990 0.0.0.0:* LISTEN 24759/./echo_mul_se
tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 1669/clickhouse-ser
tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 1249/mysqld
tcp 0 0 0.0.0.0:6379 0.0.0.0:* LISTEN 9415/redis-server *
tcp 0 0 0.0.0.0:139 0.0.0.0:* LISTEN 1145/smbd
tcp 0 0 127.0.0.1:9004 0.0.0.0:* LISTEN 1669/clickhouse-ser
tcp 0 0 127.0.0.1:9005 0.0.0.0:* LISTEN 1669/clickhouse-ser
tcp6 0 0 ::1:9009 :::* LISTEN 1669/clickhouse-ser
tcp6 0 0 ::1:8123 :::* LISTEN 1669/clickhouse-ser
tcp6 0 0 :::445 :::* LISTEN 1145/smbd
tcp6 0 0 ::1:9000 :::* LISTEN 1669/clickhouse-ser
tcp6 0 0 :::6379 :::* LISTEN 9415/redis-server *
tcp6 0 0 :::139 :::* LISTEN 1145/smbd
tcp6 0 0 ::1:9004 :::* LISTEN 1669/clickhouse-ser
tcp6 0 0 ::1:9005 :::* LISTEN 1669/clickhouse-ser
[root@VM-0-17-centos ~]# pstree -p 24759
echo_mul_server(24759)─┬─echo_mul_server(24784)
└─echo_mul_server(24799)
如上使用pstree命令查看我们的进程即可,它可以使用树状结构给你分得很明晰,而上面的netstat命令是我用来查看网络情况和进程ID的,具体命令的使用大家百度即可,另外线程的查看也可以使用pstree。
一般正常来说是客户端主动断开连接的,所以我们看到的都是正常走的,但如果服务端主动关闭程序会如何?
[root@VM-0-17-centos ~]# ./echo_mul_server 9999
第1个客户端连接上了,IP:117.61.105.77第2个客户端连接上了,IP:117.61.105.77Client: 117.61.105.77 prepare to disconnect.
第1个客户端连接上了,IP:117.61.105.77Client: 117.61.105.77 prepare to disconnect.
^C
[root@VM-0-17-centos ~]# ./echo_mul_server 9999
bind() error!
这是接着上面的程序,然后我客户端重新连接后的结果,可以看到我快捷键"ctrl+c"终止程序运行了(linux都可以这样终止,以前windows下cmd窗口不这样,现在好像也可以这样进行了),然后马上重新运行服务器开通9999端口进行服务的时候,"bind() error"了!这是为何?
这是因为在TCP连接中,主动断开连接的一方会存在一个TIME_WAIT状态,而处于这个状态的相应端口是处于使用中的状态,当我们另外的程序(包括原本使用的服务端程序)想要为服务端监听套接字绑定端口时就会出现上面的错误。
TIME_WAIT状态存在的原因是什么呢?联系上节的四次分手内容,我们知道主动断开连接的一方,它会在最后根据对方发送的FIN报文而回送一个确认报文,而发出报文的时候就基本确定本机是连接断开状态了,但对方是需要收到报文才设定自己断开连接。那如果这个确定报文没有发送给对方呢?那样本机就是已断开状态,而对方会一直等,为了防止会发生这样的情况,对方就会重新发送FIN报文,而本机处于TIME_WAIT状态的套接字就可以监听到然后重传ACK报文,对方就可以正常断开。
前面的知识和实现都是和linux系统有关的,但windows不一样,我们在windows中都是进行的多线程,那我们要实现这么一个并发服务器要怎么办?我们把这部分留到后面进行多线程实现吧,容我拖一拖,哈哈哈。
上一集|下一集