之前我们已经利用socket编程实现了一个单进程的TCP网络程序(https://blog.csdn.net/qq_34021920/article/details/80153071)
我们再开启一个终端去连接服务器端,可以发现的是第二个客户端不能和服务器正常通信了,除非我们第一个客户端退出之后,第二个客户端才能和服务器正常通信。可以注意到大部分的socket接口都是阻塞型的。实际上除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的
就像我们之前实现的代码,在accept接受了一个请求之后就会一直在while循环里面尝试去read,而没有继续去调用accept,导致不能接受到新的请求。
可以肯定的是这样的设计是不合理的,所以我们将之前的代码进行改进。
多进程版本
我们可以在一个进程接到来自客户端新的请求时就fork出一个子进程让子进程来处理,父进程只需负责监控请求的到来。这样就能做到并发处理。
直接上代码:(在这里我们只需要修改服务器端的代码即可)
tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//多进程版本
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage %s ip port\n",argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 3;
}
if(listen(sock,5)<0)
{
perror("listen");
return 4;
}
while(1)
{
struct sockaddr_in peer;
char buf[1024];
buf[0]=0;
socklen_t len=sizeof(peer);
int newsock=accept(sock,(struct sockaddr *)&peer,&len);
if(newsock<0)
{
perror("accept");
return 5;
}
inet_ntop(AF_INET,&peer.sin_addr,buf,sizeof(buf));
printf("get a connect,ip:%s,port:%d\n",buf,ntohs(peer.sin_port));
pid_t id=fork();//创建子进程
if(id<0)
{
perror("fork");
return -1;
}
else if(id==0)
{//child
close(sock);//子进程不需要sock所以关闭
if(fork()==0)//在子进程中再进行fork创建孙子进程
{ //grand_child
while(1)
{
ssize_t s=read(newsock,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
printf("[%s:%d] %s",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
}
else
{
printf("client quit\n");
close(newsock);
break;
}
write(newsock,buf,strlen(buf));
}
}
exit(0);//子进程直接退出
}
else
{//father
close(newsock);
waitpid(id,NULL,0);
}
}
close(sock);
return 0;
}
解释一下为什么要fork两次。
我们都知道,fork之后,父进程是需要等待回收子进程的,不然就会造成僵尸进程的现象。父进程一直在接受连接请求,而子进程就应该为连接上的客户提供服务。而我们在子进程中再fork一次创建出孙子进程,让孙子进程去提供服务,子进程就直接退出,父进程就可以不用等待回收子进程而继续接受连接请求。
此时我们的孙子进程就变成了孤儿进程,它的回收管理就交给1号进程了。
tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage %s ip port\n",argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
{
perror("connect");
return 3;
}
while(1)
{
char buf[1024];
buf[0]=0;
printf("Please Enter:");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
}
write(sock,buf,sizeof(buf));
if(strncmp(buf,"quit",4)==0)
{
break;
}
read(sock,buf,sizeof(buf));
printf("server echo:%s",buf);
}
close(sock);
return 0;
}
测试结果:
客户端1:
客户端2:
服务器端:
可以看到,我们蓝色框中的英文消息是客户端1发来的,黄色框中的中文消息,是客户端2发来的。实现了多连接。
多线程版本
还是只改变服务器端的代码即可
tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
//多线程版本
//为了方便传参将信息封装为结构体
typedef struct
{
int fd;
char ip[32];
int port;
}Arg;
void server(int fd,char* ip,int port)
{
char buf[1024]={0};
while(1)
{
ssize_t s=read(fd,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
printf("[%s:%d] %s",ip,port,buf);
}
else
{
printf("client quit!\n");
close(fd);
break;
}
write(fd,buf,strlen(buf));
}
}
//线程执行函数
void* thread_server(void* arg)
{
char buf[1024]={0};
Arg* p=(Arg*)arg;
server(p->fd,p->ip,p->port);//调用处理请求函数
free(p);
return NULL;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage:%s ip port",argv[0]);
return 1;
}
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 3;
}
if(listen(sock,5)<0)
{
perror("listen");
return 4;
}
while(1)
{
char buf[1024];
buf[0]=0;
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int newsock=accept(sock,(struct sockaddr *)&peer,&len);
if(newsock<0)
{
perror("accept");
return 5;
}
inet_ntop(AF_INET,&peer.sin_addr,buf,sizeof(buf));
printf("get a connect,ip:%s,port:%d\n",buf,ntohs(peer.sin_port));
pthread_t tid=0;
Arg* arg=(Arg*)malloc(sizeof(Arg));
arg->fd=newsock;
strcpy(arg->ip,buf);
arg->port=ntohs(peer.sin_port);
pthread_create(&tid,NULL,thread_server,(void*)arg);
pthread_detach(tid);//分离该线程
}
close(sock);
return 0;
}
同样的,创建线程,让线程去服务连接的客户端,而主线程就一直接受连接请求,但是主线程同样应该等待其它线程,所以我们利用pthread_detach函数分离改线程,主线程就可以不用等待别的线程而一直接受连接了。
来看结果:
客户端1
客户端2
服务器端
来比较一下多进程版本和多线程版本
多进程优点
- 可以处理多个用户
- 易于编写
- 稳定,因为进程具有独立性
多进程缺点
- 连接来了之后才创建进程,性能太低
- 多进程服务器特别吃资源,而且同时服务的客户有上限,上限也很容易达到
- 进程越多,CPU在调度时选择一个进程的周期会变长,客户等待的时间就变长。也就是切换成本大,影响性能
多线程
多线程版本的程序同样也有多进程版本的几个缺点,但是相对于进程来说,创建线程的代价要小很多,而且调度线程比调度进程的粒度要小,这样就可以降低成本,提高性能。
但是线程还有一个缺点就是线程不稳定,一个线程的退出会导致主线程直接退出。
总结一下就是,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU资源,比如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。
不论是多进程还是多线程都是来了连接请求之后才创建进程或线程,这个问题我们可以用进程池和线程池来解决。