如果实现一客户端与服务器的交互比较容易(直接用TCP的编程流程就可以实现,只是这样写出的程序只能是一个客户端交互释放连接后其他客户端才可以与服务器交互 ),但是要实现多个客户端同时与同一服务器的交互就相对复杂一点。
我们先给出服务器处理同一客户端的多次访问的伪代码:
要实现多个客户端同时与同一服务器的交互,就要求服务器与客户端的交互要并发处理。我们就想到用多进程,或者多线程来实现。以下我们分别用多进程和多线程来实现多个客户端同时与同一服务器的交互。
多进程:启动多个进程,每个进程执行和一个客户端的交互。
1. 父进程完成与客户端的连接工作,随后创建子进程,子进程与客户端具体交互。
2.父进程需不需要把文件描述符传递给子进程?(不需要,因为在fork之前打开的文件描述符,子进程会继承父进程打开的文件描述符。)
3.父进程需不需要关闭文件描述符?(需要,父进程关闭文件描述符并没有断开连接,只是将PCB中的struct file中的引用计数-1,等子进程也关闭文件描述符时才真正断开连接。而只有子进程关闭文件描述符父进程不关闭文件描述符,系统就会一直为其维护这一文件描述符,会造成资源的浪费。)
4.防止僵尸进程,需要修改信号的响应方式。
具体代码如下:
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
#include<signal.h>
void communication(int a)
{
while(1)//一个客户端与服务器多次交互
{
char buff[128]={0};
int n=recv(a,buff,127,0);
if(n<=0)
{
close(a);//关闭子进程的文件描述符
printf("One client over\n");
break;
}
printf("%d:%s\n",a,buff);
send(a,"OK",2,0);
}
}
void fun(int sig)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,fun);//防止僵尸进程
int sockfd=socket(PF_INET,SOCK_STREAM,0);
assert(-1!=sockfd);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(-1!=res);
listen(sockfd,5);
while(1)//保证服务器可以与多个客户端交互
{
int len=sizeof(cli);
int n=accept(sockfd,(struct sockaddr*)&cli,&len);
if(n<0)
{
printf("link error\n");
continue;
}
pid_t m=fork();
assert(m!=-1);
if(m==0)
{
communication(n);//与客户端交互
exit(0);//结束子进程,避免子进程再次进入while循环
}
else
{
close(n);//关闭父进程的文件描述符
}
}
close(sockfd);
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
int main()
{
int sockfd = socket(PF_INET,SOCK_STREAM,0);
assert(-1!=sockfd);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=connect(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res!=-1);
while(1)
{
printf("Please input:");
fflush(stdout);
char buff[128]={0};
fgets(buff,127,stdin);
buff[strlen(buff)-1]=0;
if(strncmp(buff,"end",3)==0)
{
break;
}
send(sockfd,buff,strlen(buff),0);
memset(buff,0,sizeof(buff));
recv(sockfd,buff,127,0);
printf("%s\n",buff);
}
close(sockfd);
}
运行结果如下:
客户端1:
客户端2:
服务器端:
我们可以看到两个客户端的文件描述符是一样的,所以能通过文件描述符来判断是否是同一个客户端连接,需要用netstat -natp来查看客户端的ip和端口(地址对)来确定客户端。
多线程:启动多个线程,每个线程执行和一个客户端的交互。
1. 主线程完成与客户端的连接工作,函数线程与客户端具体交互。
2.函数线程怎么拿到主线程的文件描述符?(创建线程时,以值传递的形式传递给函数线程,即pthread_create的参数)
3.主线程需不需要关闭文件描述符?(文件描述符是PCB中struct file*数组的下标,同进程的多个线程共享PCB,故主线程不需要关闭文件描述符。只要有一个线程关闭文件描述符,就直接关闭了与客户端的通讯)
代码实现如下:
服务器端:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
void* communication(void* arg);
int main()
{
int sockfd=socket(PF_INET,SOCK_STREAM,0);
assert(-1!=sockfd);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");
int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(-1!=res);
listen(sockfd,5);
while(1)//保证服务器可以连接多个客户端
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
{
printf("link error\n");
continue;
}
pthread_t id;
int n=pthread_create(&id,NULL,communication,(void*)c);//创建线程,将文件描述符强转为void*,此处只能是值传递,地址传递的话,可能函数线程还没拿到该地址的值,就被主线程更改
assert(n==0);
}
close(sockfd);
}
void* communication(void* arg)//函数线程完成与客户端的交互
{
while(1)//实现与一个客户端的多次交互
{
char buff[128]={0};
int c=(int)arg;//将文件描述符转回int型
int n=recv(c,buff,127,0);
if(n<=0)
{
close(c);
printf("%d client over\n",c);
break;
}
printf("%d:%s\n",c,buff);
send(c,"OK",2,0);
}
}
客户端与多进程的客户端相同。
运行结果如下:
客户端1:
客户端2:
服务器端:
多线程的一个缺点:一个进程PCB中struct file*数组的大小的确定的,所以能连接的客户端的个数比较有限。
多进程与多线程的选择??
多进程和多线程各有利弊,我们需要根据具体业务需求来选择使用哪一种。
我们从以下五个方面对多进程和多线程进行比较:
- 编程角度:多线程代码实现简单,控制也简单一些。
- 占据资源:多进程比多线程占据的资源多。
- 切换速度:进程CPU调度时比较慢,故多进程比多线程慢。
- 资源共享:线程间比进程间共享资源多。
- 安全性:进程比线程安全,因为线程共享的资源多,而进程之间是相互独立的。
- 能够创建的个数:linux中进程中能够创建的线程数量相比于系统能够创建的进程数量少得多。