服务器端实现之多进程、多线程、多路复用

1、服务器实现

在这里插入图片描述
我觉得实现了客户端之后,服务器端应该更容易。通过上面的流程图比较我们可以发现客户端和服务器端都会用到socket、write、read、close函数,服务器端只是多了bind、listen、accept三个函数,所以我们写之前只要学习这三个函数就可以了。

  • bind()函数:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
    addrlen:对应的是地址的长度。
    addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核。
  • listen()函数:int listen(int sockfd, int backlog);
    sockfd: socket()系统调用创建的要监听的socket描述字
    backlog: 相应socket可以在内核里排队的最大连接
  • accept()函数:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
    *addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
    addrlen: 返回客户端协议地址的长度
    服务器端代码和客户端代码基本流程差不多,服务器端socket后要绑定ip地址和端口,listen()监听端口,如果有客户端连接的话,就调用accept()接收客户端的请求,调用read()读取客户端发过来的数据,读取成功调write()给客户端回应,关闭客户端的套接字,最后关闭socket的套接字。代码如下:
 72         sockfd=socket(AF_INET,SOCK_STREAM,0);
 73         if(sockfd<0)
 74         {
 75                 printf("create socket failure:%s\n",strerror(errno));
 76                 return -1;
 77         }
 78         printf("create socket[%d],successfully!\n",sockfd);
 79 
 80         setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
 81         memset(&servaddr,0,sizeof(servaddr));
 82         servaddr.sin_family=AF_INET;
 83         servaddr.sin_port=htons(port);
 84         servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
 85 
 86         rv=bind(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
 87         if( rv<0 )
 88         {
 89                 printf("Socket[%d] bind on port[%d] failure: %s\n", sockfd, port, strerror(errno));
 90                 return -2;
 91         }
 92         printf("socket[%d] bind on port [%d] successfully!\n",sockfd,port,strerror(errno));
 93 
 94         listen(sockfd,13);
 95         printf("start to listen on port [%d]\n",port);
 96 
 97         while( !g_stop )
 98         {
 99                 printf("start to accept new client incoming...\n");
100 
101                 clifd=accept(sockfd,(struct sockaddr *)&cliaddr,&len);
102                 if( clifd<0 )
103                 {
104                         printf("accept new client failure:%s\n",strerror(errno));
105                         continue;
106                 }
107                 printf("Accept new client[%s:%d] successfully\n", inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
128                                 memset(buf, 0, sizeof(buf));
129                                 rv=read(clifd, buf, sizeof(buf));
130                                 if( rv < 0 )
131                                 {
132 
133                                         printf("Read data from client sockfd[%d] failure: %s\n", clifd,strerror(errno));
134                                         continue;
135                                 }
136 
137                                 else if( rv == 0)
138                                 {
139                                         printf("Socket[%d] get disconnected\n", clifd);
140                                         continue;
141 
142                                 }
143                                 else if( rv > 0 )
144                                 {
145                                         printf("Read %d bytes data from Server: %s\n", rv, buf);
146 
147 
148                                 }
84								 if( write(clifd, buf, rv) < 0 )
85								 {
86 										printf("Write %d bytes data back to client[%d] failure: %s\n", rv, client_fd,strerror(errno));
87 										close(clifd);
88 								}
89
90 								sleep(1);
91 								close(clifd);
92 						}
93 						close(sockfd);
94 }

2、多进程改写服务器

	写完服务器程序后,我们会有一个疑问,如果有多个客户端连接服务器该怎么办?这时我们就要想办法怎样才能让多个客户端同时连接,我们就会想到多进程改写服务器。多进程改写服务器实际上就是有一个客户端连接时,其它的客户端也要连,这时我们就调用fork() 函数,产生一个新的子进程,供其他客户端连接。Linux有vfork和fork系统调用,这里我用fork创建子进程,下面学习一下fork()函数:

调用fork函数后,该位置的进程会一分为二,一个为父进程,一个为子进程,如果调用成功它会有两个返回值,父进程的返回值是子进程的标志,子进程返回的是0,不成功返回-1.为什么成功调用会返回两个值?由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。所以只需要在accept之后调用fork就好了。注意的是父进程和子进程的执行顺序是交给linux内核完成的,没有规定先后顺序。父进程创建的子进程也需要资源。
多进程还可以用vfork函数:pid_t vfork(void);它的功能也是在已有的进程中创建一个新的进程,但两者创建的进程是有区别的。调用成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为无符号整型。失败:返回 -1。 fork和vfork的区别:(1)fork()父子进程执行的顺序不一定,由Linux内核决定;vfork()保证子进程先运行,子进程退出后父进程才会被调度执行;(2)fork() 子进程拷贝父进程的地址空间;vfork()子进程共享父进程的地址空间。
在这里插入图片描述

3、多线程该写服务器

我们还可以用多线程的方式实现多个客户端同时连接。首先我们要知道什么是线程,线程就是共享进程所获得的资源,所有的线程在同一块区域。
在这里插入图片描述
线程的特点:

  • 线程是资源竞争的基本单位。
    操作系统有很多资源。进程与进程之间要竞争操作系统资源,当一个进程申请得到一大堆资源。而这些资源又会分配给线程。一个进程内部有多个线程,去竞争进程所获得的资源。所以说线程是资源竞争的基本单位。
  • 线程是程序执行的最小单位
    当用户让进程去执行某个任务时,进程又会将任务细化。进程内部有很多线程,让这些线程去执行
  • 线程共享进程数据,但也拥有自己独立的一部分数据: 线程ID ,一组寄存器,栈,errno值,信号。
    创建线程:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    第三个参数start_routine是一个函数指针,它指向的函数原型是 void *func(void *),这是所创建的子线程要执行的任务(函数);
    第四个参数arg就是传给了所调用的函数的参数,如果有多个参数需要传递给子线程则需要封装到一个结构体里传进去;
    第一个参数thread是一个pthread_t类型的指针,他用来返回该线程的线程ID。每个线程都能够通过pthread_self()来获取
    自己的线程ID(pthread_t类型)。
    第二个参数是线程的属性,其类型是pthread_attr_t类型,用来设置线程的分离状态、线程栈的大小等。
    在对该属性进行设置前,我们需要先调用pthread_attr_init 函数初始化它,线程属性在使用完之后,我们应pthread_attr_destroy 把他摧毁释放。使用多线程的时候,要注意所有的线程是在用一个区域,它有可能会使数据丢失,使用互斥锁的机制来解决,当有线程要访问临界区域时,对该线程上锁,是其他线程阻塞在外面,等上一个线程完成读写操作后,在对其解锁释放,然后下一个线程也是如此,就解决了数据丢失的问题。
    线程创建
int pthread_creat(pthread_t *restrict tidp,const pthread_attr_t *attr,void*(*start_rtn)(void),void *restrict arg);   //创建线程
成功返回0,否则返回错误编号。第一个参数是新创建的线程的ID;第二个参数是设置线程的属性,为NULL是,使用默认属性。第三个参数是新创建的函数从strat_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。

线程退出

void pthread_exit(void *rval_ptr);线程通过调用此函数退出。它的作用是:终止调用它的线程并返回一个指向某个对象的指针。
int pthread_join(pthread_t thread,void **rval_ptr);成功返回0;否则返回错误编号。此函数以阻塞的方式等待thread指定的线程调用pthread_exit(),如果该线程已结束,函数立即返回。

线程同步:线程同步机制包括互斥,读写锁以及条件变量
详细介绍可以阅读该博客转载

4、多路复用改写服务器

多路复用可以用select()、poll()、epoll()函数。

  • select是单进程内可以为多个客户端服务,可以减少创建线程或进程所需要的CPU时间片或内存资源的开销,但是它有两个缺点: 一是每次调用 select()都需要把fd集合从用户态拷贝到内核态,之后内核需要遍历所有传递进来的fd,这时如果客户端fd很多时会导致系统开销很大;二是单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过setrlimit()、修改宏定义甚至重新编译内核等方式来提升这一限制,但是这样也会造成效率的降低。
  • poll()和select系统调用的本质一样,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。poll() 和 select() 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
  • epoll()是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著
    提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听
    的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。所以大多都会使用epoll()多路复用改写服务器。
    epoll和select设计实现的机制完全不同。epoll的实现分为三个部分:
    epoll_create():建立一个epoll对象。
    epoll_ctl():向epoll的对象添加套接字。
    epoll_wait():收集发生的事件的连接。

select、poll、epoll的比较

在这里插入图片描述

125         listenfd=socket_server_init(NULL,serv_port);/*把socket初始化部分封装成一个函数*/
126         if( listenfd<0 )
127         {
128                 printf("EEROR:%s server listen on port[%d] failure\n",argv[0],serv_port);
129                 return -2;
130         }
131         printf("%s server start to listen on port[%d]\n",argv[0],serv_port);
132 
133 
134         /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */
135         if( (epollfd=epoll_create(MAX_EVENTS))<0 )/*创建epoll对象*/
136         {
137                 printf("epoll_create() failure: %s\n", strerror(errno));
138                 return -3;
139         }
140         printf("epoll_create() successfully!\n");
141         event.events = EPOLLIN;/*关心事件的动作*/
142         event.data.fd = listenfd;/*将监听的事件描述符添加到事件集合*/
143 
144         if( epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event) < 0)/*注册监听对象*/
145         {
146                 printf("epoll add listen socket failure: %s\n", strerror(errno));
147                 return -4;
148         }
149 
150 
151         for( ; ;)
152         {
153                 printf("start to wait\n");
154                 events=epoll_wait(epollfd,event_array,MAX_EVENTS,-1);
155                 if( events<0 )
156                 {
157                         printf("epoll failure:%s\n",strerror(errno));
158                         break;
159                 }
160                 else if( events==0 )
161                 {
162                         printf("epoll get timeout\n");
163                         continue;
164                 }
165                 for(i=0;i<events;i++)
166                 {
167                         if ( (event_array[i].events&EPOLLERR) || (event_array[i].events&EPOLLHUP) )/*错误退出*/
168                         {
169                                 printf("epoll_wait get error on fd[%d]:%s\n",event_array[i].data.fd,strerror(errno));
170 
171                                 epoll_ctl(epollfd, EPOLL_CTL_DEL,event_array[i].data.fd,NULL);;
172                                 close(event_array[i].data.fd);
173                         }
174                         if( event_array[i].data.fd==listenfd )
175                         {
176                                 connfd=accept(listenfd,(struct sockaddr *)NULL,NULL);
177                                 if( connfd<0 )
178                                 {
179                                         printf("accept new client failure:%s\n",strerror(errno));
180                                         continue;
181                                 }
182                                 event.data.fd=connfd;
183                                 event.events=EPOLLIN;
184 
185                                 if( epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event) < 0 )
186                                 {
187                                         printf("epoll add client socket failure:%s\n",strerror(errno));
188                                         close(event_array[i].data.fd);
189                                         continue;
190                                 }
191                                 printf("epoll add client socket[%d] ok!\n",connfd);
192                         }

源码地址:(https://github.com/hero-wangjiayang/gittest)

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值