TCP编程虽然可以和多个客户交互,但是实现的是串行交互模式,就是一个客户端连接完,一个客户端再连接。但是这种模式并不能满足中国现如今的人口对于服务器的访问,即使服务器速度快,但是这样的效率还是不高的。大致如下:
那如何实现同一时刻能与多个客户端同时交互呢?使服务器和客户端交互以并发处理呢?这就是今天我们要解决的问题。也就是要使上图中红色框内的与客户端交互的流程实现并发交互。
一、多进程,多线程概念
每个进程都是一个执行流,当我们要执行多个请求时,我们就可以引入多进程,多线程了。
多进程: 启动多个进程,每个进程执行和一个客户端交互的程序;
父进程完成与客户端的连接服务,完成后,创建子进程,子进程与客户端具体交互
多线程: 启动多个线程,每个线程执行和一个客户端交互的程序;
其实概念并不难理解,我们直接看看代码如何实现吧~
二、多进程的代码实现
2.1客户端代码:
#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>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;//服务器的IP地址 端口号
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6666);//服务器上对应服务进程的端口号
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:");
char buff[128] = {0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3) == 0)
{
close(sockfd);
break;
}
send(sockfd,buff,strlen(buff)-1,0);
char recvbuff[128] = {0};
int n = recv(sockfd,recvbuff,127,0);
if(n <= 0)
{
close(sockfd);
break;
}
printf("client recv data: %s\n",recvbuff);
}
}
2.2服务器代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
void CommClient(int c)
{
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
close(c);
printf("%d over\n",c);
break;
}
printf("%d: %s \n ",c,buff);
}
}
void Zombie(int sign) //僵死进程
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,Zombie);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in cli,ser;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6666);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
while(1)
{
int cli_len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,&cli_len);
if(c < 0)
{
printf("link is error!\n");
continue;
}
pid_t n = fork();//创建子进程 c 作为文件描述符传递给子进程
assert(n != -1);
if(n == 0)
{
CommClient(c);
exit(0); //结束子进程的进程实体,PCB依旧保存
}
else //父进程
{
close(c); //仅仅是父进程关闭文件描述符,并未断开链接
}
}
close(sockfd);
}
在编写代码过程中,我们得注意:
我们创建父子进程时,如果只关闭了子进程的文件描述符,断开了子进程自己的连接,不关闭父进程的文件描述符的话会导致c描述符的位置一直被占用,c描述符是一个指针,也就会变成一个野指针。因此我们也得关闭父进程的文件描述符。并且在以后处理服务器问题时,一定要注意资源的释放的问题,不用的资源一定要及时关闭,否则会造成资源的的大量虚空浪费。
当在父进程中未关闭c文件描述符前 当在父进程中关闭c文件描述符后。
count保存了有几个进程的c指向此结构体,关系着是否为0时释放结构体资源。
接着我们来看一下结果:
也许大家会对为什么文件描述符都是产生疑问4??
那是因为系统一启动就会默认打开的0,1,2文件描述符,上图有说明,分别代表的含义。而3接着被我们的socket给占领了。所以c就会占据4号位置。而连接第二个时,也是一个全新的开始,还是会从0开始检索,到最后也是4号被占据。因此两个文件符所占的位置都是4。但是他们的端口号是唯一的,我们来看看。
linux下使用命令:ps -ef | grep cour
关闭两个客户端链接后:
可以看出两个子进程的端口号是不同的,只是拥有一个共同的父进程,这就实现了我们简单的并发操作。
三、多线程代码实现
编写代码之前我们需要明白:
1)主线程负责接收客户连接,函数线程负责处理客户连接
2) 函数线程如何拿到与客户连接的文件描述符 创建线程时,以值传递的形式传递给函数线程 (不能使用地址传递,不然有可能误改地址里的数据)
3) 主线程需不需要关闭文件描述符 不需要 用一个进程中所有线程共享进程资源,仅有独立的栈区不共享
4) 僵死进程需不需要处理?? 不需要 本身不会有子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
void* CommClient(void* data)
{
int c = (int)data;
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
close(c);
printf("%d over\n",c);
break;
}
printf("%d: %s \n ",c,buff);
send(c,"ok",2,0);
}
}
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in cli,ser;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6666);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
while(1)
{
int cli_len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,&cli_len);
printf("%x\n",&c);
if(c < 0)
{
printf("link is error!\n");
continue;
}
pthread_t id;
int res = pthread_create(&id,NULL,CommClient,(void*)c); //传值就行
assert(res == 0);
//close(c); 不能关闭 因为在共享
}
close(sockfd);
}
我们来验证下结果:
建立连接后 关闭连接后
我们可以使用命令(netstat -natp)来看一下:
上图可看出客户端和服务器的情况两两对应,都是已建立连接的情况。
我们接着使用命令看一下进程和线程:
查看进程命令:ps -ef | grep thread
查看线程命令:ps -efL | grep thread L是显示线程的
可以看出就是只有一个进程
可以看出有三个线程,他们的进程号都是6407,父进程都是3385,只有各自的线程号是不同的。主线程负责接收,函数线程负责和客户端通讯。
四、多线程和多进程的对比
根据不同的业务需求,场景进行选择
两者的比较:
- 从编程角度: 多线程代码实现简单一些,控制简单一些
- 从占据资源: 多线程比多进程小
- 从切换角度: 线程切换比进程快
- 从资源共享: 线程比进程间共享资源多比如全局,堆,文件,但同时要注意线程安全性问题。
- 从创建的数量:多进程比多线程要多很多。