多进程中通过子进程实现服务器端功能,多线程就是在子线程的回调函数中实现服务器功能而已。
其次,线程的回收这里不能使用join,跟多进程的情况一样,如果使用了join会阻塞等待回收,如果此时有新客户端访问就访问不到了。多进程的回收使用信号回收防止阻塞,线程尽量不能跟信号扯上关系,因此采用deteach将子线程分离,自动回收PCB。
deteach相比join有个坏处就是不能查看回调函数的返回值(join通过参数2void**来回收),所以还有个方法:再新建一个子线程,用该子线程使用Join回收(线程可以兄弟之间相互回收),主线程不需要阻塞
多路I/O转接服务器
select
内核相当于秘书,监听有没有客户端连接、有没有数据传过来。
有客户端想连接了,内核汇报给服务器端,服务器端再accept。这样就避免了一直在accept阻塞。
函数解析#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
- 参数1:nfds,永远是监听的所有文件描述符中, 最大文件描述符+1(内部是一个for循环)
- 参数2: fd_set *readfds,传入传出参数,是一个位图。
传入表示:要监听哪些文件描述符的读事件,set的位图上置1传入。
传出表示有哪些被监听的文件描述符活跃了。
客户端连接请求是通过读取套接字上的数据来完成的,所以接客fd也被放在这个读集合。客户端往服务器写的数据,服务器也是读。
比如这里传入 3.5.6号文件描述符,表示要监听3.5.6,传入位图3.5.6为1。传出只有5。6为1,因为监听的3.5.6中只有5.6活跃了。
- 参数5struct timeval *timeout,设置监听时常,NULL为一直监听,0监听一次后立马返回,常数位监听常数秒。
返回值:
>0:3个监听集合,监听到活跃的fd总数。
=0: 三个集合加起来,没有活跃的fd
-1: errno
对fd_set文件描述符监听集合的操作函数
**void FD_ZERO(fd_set *set); --将监听集合置0**
** fd_set rset; FD_ZERO(&rset); //将rset集合清空**
**void FD_SET(int fd, fd_set *set); --将fd添加到监听集合中(set想监听谁添加谁)**
** FD_SET(3,&rset);FD_SET(5,&rset);FD_SET(6,&rset); //将文件描述符3和5加到rset集合中**
**void FD_CLR(int fd, fd_set *set); --将一个fd从监听集合中移除**
(某个客户端关闭了,不需要监听了,就移除它的套接字fd)**int FD_ISSET(int fd, fd_set *set); --判断一个fd是否在监听集合中**
** 在返回1,不在返回0**
思路分析
int maxfd = 0;
lfd = socket() ; //创建套接字
maxfd = lfd; // 这个maxfd用来后面的select函数,以及for
bind(); //绑定地址结构
listen(); //设置监听上限
fd_set rset, allset; //创建r监听集合,allset记录每次要监听的集合
//因为rset会因为传出而变成监听到的活跃fd集合,所以用allset来复位rset,用于下次循环的监听列表
FD_ZERO(&allset); //将r监听集合清空
FD_SET(lfd, &allset); //将 lfd 添加至读集合中, lfd接客套接字一定是第一个要监听的
while(1) {
rset = allset; //select会改变rset,所以保存监听集合到allset
ret = select(maxfd+1, &rset, NULL, NULL, NULL); //监听
if(ret > 0) { //有监听的描述符活跃
if (FD_ISSET(lfd, &rset)) { // 1 在,0不在。lfd在rset,说明有客户端要连接
cfd = accept(); //建立连接,返回用于通信的文件描述符
maxfd = cfd; // 有新的fd产生,最大文件描述符就是新的fd了
//这个maxfd在后面的for遍历文件描述符时时有用
// 避免for1024次(文件描述符最多定义1024个)
FD_SET(cfd, &allset); //把刚连上的客户端套接字fd添加到监听通信描述符集合
}
for (i = lfd+1; i <= maxfd; i++){ // lfd后面的fd都是通讯套接字fd
FD_ISSET(i, &rset) //在传出活跃fd的rset中比对谁是活跃的fd
read() // 读活跃的fd
小 -- 大
write();
}
}
}
代码实现
int main()
{
int lfd, cfd, ret, len, j, i;
char buf[BUFSIZ];
//地址结构体
struct sockaddr_in serve_addr, client_addr;
socklen_t client_addr_len;
bzero(&serve_addr,sizeof(serve_addr)); //结构体清空
serve_addr.sin_family = AF_INET;
serve_addr.sin_port = htons(SERVE_PORT);
serve_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//创建socket
lfd = socket(AF_INET,SOCK_STREAM,0);
int maxfd = lfd; //最大的文件描述符,用作读的上限以及select的参数
if(lfd==-1)
{
perror("socket error");
exit(1);
}
//绑定ip和端口
bind(lfd,(struct sockaddr *)&serve_addr,sizeof(serve_addr));
//设置上限
listen(lfd,128);
fd_set rset, allset; //设置监听的集合
FD_ZERO(&allset); //清空集合
FD_SET(lfd,&allset); //将lfd加入到监听集合
while(1)
{
rset = allset;
ret = select(maxfd+1,&rset,NULL,NULL,NULL);
if(ret<0)
{
perror("select error");
exit(1);
}
if(FD_ISSET(lfd,&rset)) //如果lfd在传出的rset中,表示有客户端要进行连接
{
client_addr_len = sizeof(client_addr);
cfd = accept(lfd,(struct sockaddr *)&client_addr,&client_addr_len);
FD_SET(cfd,&allset); //将cfd加入到监听集合
if(maxfd<cfd)
maxfd = cfd; //更新最大的文件描述符
//说明select只返回了lfd这一个,后续指令无需执行,continue跳出本次,继续while
if(ret==1)// 这个判断是在lfd活跃的if里面,如果lfd活跃并且ret=1,
// 说明除了lfd没有其他套接字fd活跃,就不需要后面的for了
continue;
}
for(i=lfd+1; i<=maxfd; i++) // 遍历lfd后面的套接字fd,看是否活跃
{
if(FD_ISSET(i,&rset)){ //找到满足读事件的那个描述符
len = read(i,buf,sizeof(buf));
if(len==0) //检测到客户端关闭了连接
{
close(i); //关闭该描述符
FD_CLR(i,&allset); //把断开的fd从监听中移除
}
//如果len不为0就表示有数据,循环更改数据
for(j=0; j<len; j++)
buf[j] = toupper(buf[j]);
//写回更改后的数据
write(i,buf,len);
write(STDOUT_FILENO,buf,len);
}
}
}
close(lfd);
return 0;
}
select优缺点
缺点
如果我想监听 3、1023,那么在后续的for查看哪些fd活跃的时候,for的判断条件就是1023了。然而中间的部分我并不想监听,这样效率就很低了。(当然,可以自定义查询数组,就是麻烦点)
- 文件描述符最多1024个,排除0,1,2,只剩下1024-3=1021个。所以监听上限受限
优点
跨平台,多个平台都可以用。win。linux。MacOS。unix。类unix。mips
自定义监听数组实现select
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 端口复用
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
while (1) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
if (--nready == 0) // 如果nready =1说明只有lfd活跃,不用读,直接进入下次的while循环
continue;
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue; // 说明client[i]要么被关闭要么没活跃,进入下层for
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
client[i] = -1; //关闭的cfd置为-1,下次就不读它了
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0) // 上面的nready=1是只有lfd,直接Continue到下层while了,这里的nready =1 说明不是因为lfd活跃,而是cfd活跃了,并且只有一个cfd活跃,那么既然读完了就没必要再往下试探有没有其他cfd活跃了
break; /* 跳出for, 但还在while中 */
}
}
}
Close(listenfd);
return 0;
}
初始化client[]数组全为-1
1、lfd收到新客户端连接后将cfd加入监听数组
用client[]数组放要新客户端fd,并且用一个变量maxi记录数组的最大下标
2、查询有哪些fd活跃了
遍历client数组,取出来,如果client[i] < 0 说明不是监听数组的。如果 >0就read,如果read到0说明要关闭连接,那就把对应client[i]置为-1,下次遍历client就不会遍历它了。