进程和线程的使用在前面博文已经讲述完毕,在完成一个最简单的服务器之后,就是要考虑下如何实现并发服务器了。 要实现服务的并发,只能通过进程和线程两种方式。 之前提到过listen_fd和connect_fd,listen用于监听是否有客户端连接,维护两个fd队列,没完成握手的和完成就绪的。 connect从就绪队列取描述符,这个connect_fd描述符将用于数据通信,所以要实现并发,就是将connect_fd分发到线程或进程上,由他们去独立完成通信。 在实际并发服务器应用场合,在IO层大多通过两个地方来提高代码效率,一个是描述符处理,一个是线程/进程调度处理。
下图简单描述了并发服务器的原理:
在处理IO时,会用到IO复用技术提高效率,在线程/进程分配时,会先构造线程池或进程池,并以某种方式调度,这些在后续博文详细描述。
下面是并发实现的简单代码,利用线程和进程实现服务器的并发。 进程实现:
/* File Name: server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <netinet/in.h>
const int DEFAULT_PORT = 2500;
const int BUFFSIZE = 1024;
const int MAXLINK = 10;
class sigOp
{
public:
void addSigProcess(int sig,void (*func)(int));
void sendSig(const int sig, const int pid);
};
void sigOp::addSigProcess(int sig,void (*func)(int))
{
struct sigaction stuSig;
memset(&stuSig, '\0', sizeof(stuSig));
stuSig.sa_handler = func;
stuSig.sa_flags |= SA_RESTART;
sigfillset(&stuSig.sa_mask);
sigaction(sig, &stuSig, NULL);
}
void sigOp::sendSig(const int sig, const int pid)
{
kill(pid, sig);
}
void waitchlid(int sig)
{
pid_t pid;
int stat;
while((pid = waitpid(-1, &stat, WNOHANG)) > 0);
}
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[BUFFSIZE];
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(DEFAULT_PORT);
if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
if (listen(socket_fd, MAXLINK) == -1)
{
exit(0);
}
sigOp sig;
sig.addSigProcess(SIGCHLD, waitchlid);//回收进程
while(1)
{
if ((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1)
{
break;
}
else
{
if (0 == fork())
{
close(socket_fd); //关闭监听描述符
while(1)
{
memset(&buff,'\0', sizeof(buff));
recv(connect_fd, buff, BUFFSIZE - 1, 0);
send(connect_fd, buff, BUFFSIZE - 1, 0);
printf("recv msg from client: %s\n", buff);
}
close(connect_fd);
exit(0);
}
close(connect_fd);//父进程关闭连接描述符
}
}
close(connect_fd);
close(socket_fd);
}
之前提到过listen描述符和connect描述符是完全独立的,关闭都不会影响另一个描述符工作。所以在代码中,父子进程都会关闭不需要的描述符。 测试结果如下:
ps -aux查看系统进程,可以看到三个进程,一个是主进程用于listen监听,两个子进程进行通信。
下面是线程并发实现:
/* File Name: server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/unistd.h>
#include <netinet/in.h>
const int DEFAULT_PORT = 2500;
const int BUFFSIZE = 1024;
const int MAXLINK = 10;
void* run(void* pConnect_fd)
{
char buff[BUFFSIZE];
int *connect_fd = reinterpret_cast<int *>(pConnect_fd);
pthread_detach(pthread_self());
while(1)
{
memset(&buff,'\0', sizeof(buff));
recv(*connect_fd, buff, BUFFSIZE - 1, 0);
send(*connect_fd, buff, BUFFSIZE - 1, 0);
printf("recv msg from client: %s\n", buff);
}
}
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(DEFAULT_PORT);
if (bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
exit(0);
}
if (listen(socket_fd, MAXLINK) == -1)
{
exit(0);
}
while(1)
{
if ((connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1)
{
break;
}
else //accept获取到描述符创建线程
{
pthread_t _Tread;
pthread_create(&_Tread, NULL, run, &connect_fd);//传参为连接描述符
}
}
close(connect_fd);
close(socket_fd);
}
测试结果如下:
效果和进程一样,执行netstat查看tcp状态
两组连接相互通信。
线程并发和进程并发各有优劣,目前大多服务器还是用线程进行并发的,进程要对父进程进行拷贝,资源消耗大,但相互直接资源互不影响,线程效率高但是要注意锁的使用,一个线程可能会影响整个服务器的运行。 详细优缺点详细可参考:http://blog.chinaunix.net/uid-20556054-id-3067672.html