Linux网络编程2-多进程和多线程版本服务器
- 1.套接字相关函数的封装wrap.h wrap.c
- 2.支持多并发的服务器
- 3.多进程版本分析
- 4.多进程版本实现
- 5.多线程版本分析
- 6.多线程版本实现
1.套接字相关函数的封装wrap.h wrap.c
像accept,read这样的能够引起阻塞的函数,若被信号打断,由于信号的优先级较高, 会优先处理信号, 信号处理完成后,会使accept或者read解除阻塞, 然后返回, 此时返回值为 -1,设置errno=EINTR;
在/usr/include/asm-generic/errno.h文件中包含了errno所有的宏和对应的错误描述信息.
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>void perr_exit(const char *s)
{perror(s);exit(-1);
}int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{int n;again:if ((n = accept(fd, sa, salenptr)) < 0) {if ((errno == ECONNABORTED) || (errno == EINTR))goto again;elseperr_exit("accept error");}return n;
}int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{int n;if ((n = bind(fd, sa, salen)) < 0)perr_exit("bind error");return n;
}int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{int n;if ((n = connect(fd, sa, salen)) < 0)perr_exit("connect error");return n;
}int Listen(int fd, int backlog)
{int n;if ((n = listen(fd, backlog)) < 0)perr_exit("listen error");return n;
}int Socket(int family, int type, int protocol)
{int n;if ((n = socket(family, type, protocol)) < 0)perr_exit("socket error");return n;
}ssize_t Read(int fd, void *ptr, size_t nbytes)
{ssize_t n;again:if ( (n = read(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n;
}ssize_t Write(int fd, const void *ptr, size_t nbytes)
{ssize_t n;again:if ( (n = write(fd, ptr, nbytes)) == -1) {if (errno == EINTR)goto again;elsereturn -1;}return n;
}int Close(int fd)
{int n;if ((n = close(fd)) == -1)perr_exit("close error");return n;
}/*参三: 应该读取的字节数*/
ssize_t Readn(int fd, void *vptr, size_t n)
{size_t nleft; //usigned int 剩余未读取的字节数ssize_t nread; //int 实际读到的字节数char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ((nread = read(fd, ptr, nleft)) < 0) {if (errno == EINTR)nread = 0;elsereturn -1;} else if (nread == 0)break;nleft -= nread;ptr += nread;}return n - nleft;
}ssize_t Writen(int fd, const void *vptr, size_t n)
{size_t nleft;ssize_t nwritten;const char *ptr;ptr = vptr;nleft = n;while (nleft > 0) {if ( (nwritten = write(fd, ptr, nleft)) <= 0) {if (nwritten < 0 && errno == EINTR)nwritten = 0;elsereturn -1;}nleft -= nwritten;ptr += nwritten;}return n;
}static ssize_t my_read(int fd, char *ptr)
{static int read_cnt;static char *read_ptr;static char read_buf[100];if (read_cnt <= 0) {
again:if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {if (errno == EINTR)goto again;return -1;} else if (read_cnt == 0)return 0;read_ptr = read_buf;}read_cnt--;*ptr = *read_ptr++;return 1;
}ssize_t Readline(int fd, void *vptr, size_t maxlen)
{ssize_t n, rc;char c, *ptr;ptr = vptr;for (n = 1; n < maxlen; n++) {if ( (rc = my_read(fd, &c)) == 1) {*ptr++ = c;if (c == '\n')break;} else if (rc == 0) {*ptr = 0;return n - 1;} elsereturn -1;}*ptr = 0;return n;
}int tcp4bind(short port,const char *IP)
{struct sockaddr_in serv_addr;int lfd = Socket(AF_INET,SOCK_STREAM,0);bzero(&serv_addr,sizeof(serv_addr));if(IP == NULL){//如果这样使用 0.0.0.0,任意ip将可以连接serv_addr.sin_addr.s_addr = INADDR_ANY;}else{if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){perror(IP);//转换失败exit(1);}}serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(port);Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));return lfd;
}
2.支持多并发的服务器
如何支持多个客户端—支持多并发的服务器
由于accept和read函数都会阻塞, 如当read的时候, 不能调用accept接受新的连接, 当accept阻塞等待的时候不能read读数据.
第一种方案: 使用多进程, 可以让父进程接受新连接, 让子进程处理与客户端通信
思路: 让父进程accept接受新连接, 然后fork子进程, 让子进程处理通信, 子进程处理完成后退出, 父进程使用SIGCHLD信号回收子进程.
第二种方案: 使用多线程, 让主线程接受新连接, 让子线程处理与客户端通信; 使用多线程要将线程设置为分离属性, 让线程在退出之后自己回收资源.
如何不使用多进程或者多线程完成多个客户端的连接请求?
可以将accept和read函数设置为非阻塞, 调用fcntl函数可以将文件描述符设置为非阻塞, 让后再while循环中忙轮询.
3.多进程版本分析
阻塞函数在阻塞期间若收到信号,会被信号终端,errno设置为EINTR,这个错误不应该看成一个错误.
while(1)
{cfd = accept();while(1){n = read(cfd, buf, sizeof(buf));if(n<=0){break;}}
}
解决办法1:
将cfd设置为非阻塞: fcntl
缺点:假如有多个客户端连接请求, cfd只会保留最后一个文件描述符的值
解决方法2:
使用多进程: 让父进程监听接受新的连接, 子进程处理新的连接(接收和发送数据); 父进程还负责回收子进程
多进程版本思路:子进程复制父进程的文件描述符
//1 创建socket, 得到一个监听的文件描述符lfd---socket()
//2 将lfd和IP和端口port进行绑定-----bind();
//3 设置监听----listen()
//4 进入while
while(1)
{//等待有新的客户端连接到来cfd = accept();//fork一个子进程, 让子进程去处理数据pid = fork();if(pid<0){exit(-1);}else if(pid>0){//关闭通信文件描述符cfdclose(cfd);}else if(pid==0){//关闭监听文件描述符close(lfd);//收发数据while(1){//读数据n = read(cfd, buf, sizeof(buf));if(n<=0){break;}//发送数据给对方write(cfd, buf, n);}close(cfd);//下面的exit必须有, 防止子进程再去创建子进程exit(0);}
}
close(lfd);
还需要添加的功能: 父进程使用SIGCHLD信号完成对子进程的回收
注意点: accept或者read函数是阻塞函数,会被信号打断,此时不应该视为一个错误。errno=EINTR
父子进程能够共享的:
文件描述符(子进程复制父进程的文件描述符)
mmap共享映射区
4.多进程版本实现
//多进程版本的网络服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"int main()
{//创建socketint lfd = Socket(AF_INET, SOCK_STREAM, 0);//绑定struct sockaddr_in serv;bzero(&serv, sizeof(serv));serv.sin_family = AF_INET;serv.sin_port = htons(8888);serv.sin_addr.s_addr = htonl(INADDR_ANY);Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));//设置监听Listen(lfd, 128);pid_t pid;int cfd;char sIP[16];socklen_t len;struct sockaddr_in client;while(1){//接受新的连接len = sizeof(client);memset(sIP, 0x00, sizeof(sIP));cfd = Accept(lfd, (struct sockaddr *)&client, &len);printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));//接受一个新的连接, 创建一个子进程,让子进程完成数据的收发操作pid = fork();if(pid<0){perror("fork error");exit(-1);}else if(pid>0){//关闭通信文件描述符cfdclose(cfd); }else if(pid==0){//关闭监听文件描述符close(lfd);int i=0;int n;char buf[1024];while(1){//读数据n = Read(cfd, buf, sizeof(buf));if(n<=0){printf("read error or client closed, n==[%d]\n", n);break;}//printf("client:[%s] [%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));printf("[%d]---->:n==[%d], buf==[%s]\n", ntohs(client.sin_port), n, buf);//将小写转换为大写for(i=0; i<n; i++){buf[i] = toupper(buf[i]);}//发送数据Write(cfd, buf, n);}//关闭cfdclose(cfd);exit(0);}}//关闭监听文件描述符close(lfd);return 0;
}
改进:增加对子进程的回收:
//多进程版本的服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/wait.h>
#include <ctype.h>
#include "wrap.h"//信号处理函数
void waitchild(int signo)
{pid_t wpid;while(1){wpid = waitpid(-1, NULL, WNOHANG);if(wpid>0){printf("child exit, wpid==[%d]\n", wpid);}else if(wpid==0 || wpid==-1){break;}}
}int main()
{//创建socketint lfd = Socket(AF_INET, SOCK_STREAM, 0);//设置端口复用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));//绑定-bindstruct sockaddr_in serv;serv.sin_family = AF_INET;serv.sin_port = htons(8888);serv.sin_addr.s_addr = htonl(INADDR_ANY);Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));//监听-listenListen(lfd, 128);//阻塞SIGCHLD信号sigset_t mask;sigemptyset(&mask);sigaddset(&mask, SIGCHLD);sigprocmask(SIG_BLOCK, &mask, NULL);int cfd;socklen_t len;char sIP[16];pid_t pid;struct sockaddr_in client;while(1){//等待客户端连接--acceptmemset(sIP, 0x00, sizeof(sIP));len = sizeof(client);bzero(&client, sizeof(client));cfd = Accept(lfd, (struct sockaddr *)&client, &len); printf("client-->[%s]:[%d]\n", inet_ntop(AF_INET, &client.sin_addr.s_addr, sIP, sizeof(sIP)), ntohs(client.sin_port));//创建子进程pid = fork();if(pid<0){perror("fork error");close(lfd);return -1;} else if(pid>0) //父进程{//关闭通信的文件描述符close(cfd);//注册SIGCHLD信号处理函数struct sigaction act;act.sa_handler = waitchild;act.sa_flags = 0;sigemptyset(&act.sa_mask);sigaction(SIGCHLD, &act, NULL);//解除对SIGCHLD信号的阻塞sigprocmask(SIG_UNBLOCK, &mask, NULL);}else if(pid==0) //子进程{//关闭监听文件描述符close(lfd); int i = 0;int n = 0;char buf[1024];while(1){memset(buf, 0x00, sizeof(buf));n = Read(cfd, buf, sizeof(buf));if(n<=0) {printf("read error or client closed, n==[%d]\n", n);break; }printf("read over, n==[%d],buf==[%s]\n", n, buf);for(i=0; i<n; i++){buf[i] = toupper(buf[i]);}write(cfd, buf, n);}close(cfd);exit(0);}}//关闭监听文件描述符close(lfd);return 0;
}
5.多线程版本分析
//1 创建socket, 得到一个监听的文件描述符lfd---socket()
//2 将lfd和IP和端口port进行绑定-----bind();
//3 设置监听----listen()
//4 while循环
while(1)
{//接受新的客户端连接请求cfd = accept();//创建一个子线程pthread_create(&threadID, NULL, thread_work, &cfd);//设置线程为分离属性pthread_detach(threadID);}
close(lfd);void *thread_work(void *arg)
{//获得参数: 通信文件描述符int cfd = *(int *)arg;while(1){//读数据n = read(cfd, buf, sizeof(buf));if(n<=0){break;}//发送数据write(cfd, buf, n);}close(cfd);
}
问题:
1 子线程能否关闭lfd?
子线程不能关闭监听文件描述符lfd,原因是子线程和主线程共享文件描述符,而不是复制的.
2 主线程能否关闭cfd?
主线程不能关闭cfd,主线程和子线程共享一个cfd,而不是复制的,close之后cfd就会被真正关闭.
3 多个子线程共享cfd, 会有什么问题发生?
只有一个子线程能够通信。
6.多线程版本实现
//多线程版本的高并发服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include "wrap.h"//子线程回调函数
void *thread_work(void *arg)
{sleep(20);int cfd = *(int *)arg;printf("cfd==[%d]\n", cfd);int i;int n;char buf[1024];while(1){//read数据memset(buf, 0x00, sizeof(buf));n = Read(cfd, buf, sizeof(buf));if(n<=0){printf("read error or client closed,n==[%d]\n", n);break;}printf("n==[%d], buf==[%s]\n", n, buf);for(i=0; i<n; i++){buf[i] = toupper(buf[i]);}//发送数据给客户端Write(cfd, buf, n); }//关闭通信文件描述符close(cfd);pthread_exit(NULL);
}int main()
{//创建socketint lfd = Socket(AF_INET, SOCK_STREAM, 0);//设置端口复用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));//绑定struct sockaddr_in serv;bzero(&serv, sizeof(serv));serv.sin_family = AF_INET;serv.sin_port = htons(8888);serv.sin_addr.s_addr = htonl(INADDR_ANY);Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));//设置监听Listen(lfd, 128);int cfd;pthread_t threadID;while(1){//接受新的连接cfd = Accept(lfd, NULL, NULL);//创建子线程pthread_create(&threadID, NULL, thread_work, &cfd);//设置子线程为分离属性pthread_detach(threadID);}//关闭监听文件描述符close(lfd);return 0;
}
上面代码中,accept接收3个连接,创建3个子线程,这些子线程共享threadId结构与cfd变量,因此需要一个线程对应一个。数组。
// 创建100个INFO结构,最多可以接收100个连接
struct INFO
{int cfd;pthread_t threadID;struct sockaddr_in client;
};
struct INFO info[100];//初始化INFO数组
for(i=0; i<100; i++)
{info[i].cfd=-1;
}// 接收连接以后,从数组中分配一个INFO结构
for(i=0; i<100; i++)
{if(info[i].cfd==-1){//这块内存可以使用}
}
if(i==100) // 没有空闲的INFO结构,拒绝接收新的连接
{//拒绝接受新的连接close(cfd);
}
//多线程版本的服务器
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <pthread.h>
#include "wrap.h"typedef struct info
{int cfd; //若为-1表示可用, 大于0表示已被占用int idx;pthread_t thread;struct sockaddr_in client;
}INFO;INFO thInfo[1024];//线程执行函数
void *thread_work(void *arg)
{INFO *p = (INFO *)arg;printf("idx==[%d]\n", p->idx);char sIP[16];memset(sIP, 0x00, sizeof(sIP));printf("new client:[%s][%d]\n", inet_ntop(AF_INET, &(p->client.sin_addr.s_addr), sIP, sizeof(sIP)), ntohs(p->client.sin_port));int n;int cfd = p->cfd;struct sockaddr_in client;memcpy(&client, &(p->client), sizeof(client));char buf[1024];while(1){memset(buf, 0x00, sizeof(buf));//读数据n = Read(cfd, buf, sizeof(buf));if(n<=0){printf("read error or client closed, n==[%d]\n", n);Close(cfd);p->cfd =-1; //设置为-1表示该位置可用pthread_exit(NULL);}for(int i=0; i<n; i++){buf[i] = toupper(buf[i]);}//发送数据Write(cfd, buf, n);}
}void init_thInfo()
{int i = 0;for(i=0; i<1024; i++){thInfo[i].cfd = -1;;}
}int findIndex()
{int i;for(i=0; i<1024; i++){if(thInfo[i].cfd==-1){break;}}if(i==1024){return -1;}return i;
}int main()
{//创建socketint lfd = Socket(AF_INET, SOCK_STREAM, 0);//设置端口复用int opt = 1;setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));//绑定--将lfd 和 IP PORT绑定struct sockaddr_in serv;bzero(&serv, sizeof(serv));serv.sin_family = AF_INET;serv.sin_port = htons(8888);serv.sin_addr.s_addr = htonl(INADDR_ANY);Bind(lfd, (struct sockaddr *)&serv, sizeof(serv));//监听Listen(lfd, 128);//初始化init_thInfo();int cfd;int ret;int idx;socklen_t len;pthread_t thread;struct sockaddr_in client;while(1){len = sizeof(client);bzero(&client, sizeof(client));//获得一个新的连接cfd = Accept(lfd, (struct sockaddr *)&client, &len);//创建一个子进程, 让子进程处理连接---接收数据和发送数据//找数组中空闲的位置idx = findIndex();if(idx==-1){Close(cfd);continue;}//对空闲位置的元素的成员赋值thInfo[idx].cfd = cfd;thInfo[idx].idx = idx;memcpy(&thInfo[idx].client, &client, sizeof(client));//创建子线程---该子线程完成对数据的收发ret = pthread_create(&thInfo[idx].thread, NULL, thread_work, &thInfo[idx]);if(ret!=0){printf("create thread error:[%s]\n", strerror(ret));exit(-1);}//设置子线程为分离属性pthread_detach(thInfo[idx].thread);}Close(lfd);return 0;}