网络编程(2)—多客户端交互

发布于:2025-08-30 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.多客户端

1.单循环服务器

socket     //listenfd 
bind       //绑定服务器端的地址 
listen     //监听客户端的连接请求 --- 请求队列 
while(1)  //提取多个客户端的连接请求 建立连接 
{
    confd = accpet //请求队列中提取已连接的请求 返回连接好的socket的fd
    //通信
    while(1)
    {
      read
      sprintf
      write
    }
}

单循环服务器特点:

1.可以处理多个客户端 (不能同时) 

2.效率不高

2.并发服务器

socket     //listenfd 
bind       //绑定服务器端的地址 
listen     //监听客户端的连接请求 --- 请求队列 
while(1)  //提取多个客户端的连接请求 建立连接 
{
    confd = accpet //请求队列中提取已连接的请求 返回连接好的socket的fd
    //通信
    
    //进程
    pid = fork();
    if (pid < 0)
    {
       perror("fork fail");
       return -1;
    }
    if (pid == 0) //子进程 
    {
       while(1)
      {
       read
       sprintf
       write
       }
    }
}

 :当每次服务器端按ctrl+c结束时,马上再运行server.c,会导致无法连接到原地址,此时需要用到setsockopt

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
功能:
     设置socket的属性

参数:     
   @sockfd   --- 要设置的socket 
   @level    --- 设置socket层次 //socket本身 tcp ip 
   @optname  --- 选项名字 
   @optval   --- 选项值 
   @optlen   --- 长度 

设置一个选项(开启一个功能) ---让地址重用

用法:

int on = 1;
 setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on, sizeof(int));  //表示 对 listenfd这个socket 开启 地址重用的功能 

1.多进程接收多客户端

创建多个进程来实现接收多个客户端

#include "head.h"

void do_child (int signo)
{
	wait(NULL); 
}


int main(int argc, char const *argv[])
{
	//step1 socket 
	int fd = socket(AF_INET,SOCK_STREAM,0);

	if (fd < 0)
	{
		perror("socket fail");
		return -1;
	}

	struct sockaddr_in seraddr;
	bzero(&seraddr,sizeof(seraddr));
	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(50000);
	seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	int on = 1;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&on, sizeof(int));   
	//step2 bind 
	if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0)
	{
		perror("connect fail");
		return -1;
	}
	
	//step3 listen
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}
	struct sockaddr_in cliaddr;
	bzero(&cliaddr,0);
	socklen_t len = sizeof(cliaddr);
	//step4 accept
	signal(SIGCHLD,do_child);
	while (1)
	{
		int connfd = accept(fd,(struct sockaddr *)&cliaddr,&len);
		if (connfd < 0)
		{
			perror("accept fail");
			return -1;
		}
		printf("---client connect---\n");
		printf("client ip:%s\n",inet_ntoa(cliaddr.sin_addr));
		printf("port: %d\n",ntohs(cliaddr.sin_port));

		//创建 子进程 
		//让子进程去通信 
		pid_t pid = fork();
		if (pid < 0)
		{
			perror("fork fail");
			return -1;
		}
		if (pid == 0)
		{
			char buf[1024];
			while(1)
			{
				recv(connfd,buf,sizeof(buf),0);
				printf("buf = %s\n",buf );
				if (strncmp(buf,".quit",5) == 0)
				{
					close(connfd);
					exit(0);
				}
			}
		}

	}
	return 0;
}
2.多线程接收多客户端

创建多个线程来实现接收多个客户端

#include "head.h"

void do_child (int signo)
{
	wait(NULL); 
}

void *thread(char *arg)
{
    int connfd = *(int *)arg;
    char str[256] = {0};

    while(1)
    {

        recv(connfd, str, sizeof(str), 0);
        printf("rec:%s\n", str);
        if(0 == strcmp(str, ".quit"))
        {
            close(connfd);
            return NULL;
        }

    }

}

int main(int argc, char const *argv[])
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd  < 0)
    {
        perror("fail to socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    bzero(&seraddr, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int));
    if(bind(listenfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)) < 0)
    {
        perror("fail to bind");
        return -1;
    }

    if(listen(listenfd, 5) < 0)
    {
        perror("fail to listen");
        return -1;
    }

    struct sockaddr_in cliaddr;
    bzero(&cliaddr, sizeof(cliaddr));
    socklen_t len = sizeof(cliaddr);
    pthread_t tid;
    while(1)
    {
        int connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);
        if(connfd < 0)
        {
            perror("fail to accept");
            return -1;
        }
        printf("连接成功\n");

        int ret = pthread_create(&tid, NULL, thread, &connfd);
        if(ret != 0)
        {
            errno = ret;
            perror("pthread_create fail");
            return -1;
        }
        pthread_detach(tid);
    }

    close(listenfd);

    return 0;
}

:并发服务器 ---多进程方式的效率 肯定 低于 多线程 

3.多路IO复用

1.阻塞IO模型 
  • scanf
  • getchar
  • fgets
  • read
  • recv

以读为例:读操作--->内核中读取数据--->如果没有数据,一直等到,直到有数据--->之后将数据带回到用户空间

2.非阻塞IO模型

以读为例:读操作--->内核中读取数据--->如果没有数据,不等,直接返回用户空间

设置非阻塞:

函数接口:

  • fcntl

int fcntl(int fd, int cmd, ... /* arg */ );
功能:
   维护文件描述符
参数:
  @fd  --- 要操作的fd
  @cmd --- 要做的一些操作 //command
  @... --- 可变参数 
返回值 
  取决于所做的操作 

用法:

   int flags;
   flags = fcntl(fd,F_GETFL,0); //读文件描述符 
   flags = flags | O_NONBLOCK;  //修改为非阻塞
   fcntl(fd,F_SETFL,flags);     //将非阻塞写入文件描述符 

3.信号驱动IO 

    int flags = fcntl(fd, F_GETFL);
    flags = flags | O_ASYNC;          //开启异步信号
    fcntl(fd, F_GETFL, flags);

    fcntl(fd, F_SETOWN, getpid());    //设置和进程相关联

    signal(SIGIO, do_handler);       //signal SIGIO的处理函数

4.多路复用IO
1.select

int select(int nfds, 
      fd_set *readfds, 
      fd_set *writefds,
      fd_set *exceptfds, 
      struct timeval *timeout
      );

功能:
   实现IO多路复用 

@nfds      //是关心的文件描述符中最大的那个文件描述符 + 1  
@readfds   //代表 要关心 的 读操作的文件描述符的集合 
@writefds  //代表 要关心 的 写操作的文件描述符的集合
@exceptfds //代表 要关心 的 异常的文件描述符的集合
@timeout   //超时 --- 设置一个超时时间 
           //NULL 表示select是一个阻塞调用 
           //设置时间 
           //         0 --- 非阻塞 
           //    n (>0) --- 阻塞n这么长时间 
           //注意: 这个值 每次 自动在往下减少 --直到减少到0
           
struct timeval
           struct timeval {
               long    tv_sec;         /* seconds */
               long    tv_usec;        /* microseconds */
           };
           
           struct timeval t = {0,0};

返回值:
   成功 返回就绪的文件描述符的数量 
   失败 -1 

使用:

1.建立一张表  监控
     fd_set readfds; //一张表               
     FD_ZERO(&readfds); //清空这张表              

2.将要监控的文件描述符 添加表中 
    FD_SET(0,&readfds);
    FD_SET(fd,&readfds);

3. nfds = fd + 1; 
select(nfs,&readfds,NULL,NULL,NULL)
void FD_CLR(int fd, fd_set *set); //将fd从set集合中清除 
int  FD_ISSET(int fd, fd_set *set);//判断fd是否在set中 
void FD_SET(int fd, fd_set *set);//将fd添加到set集合中
void FD_ZERO(fd_set *set);//将set集合清空 

多路IO复用

listenfd = socket
bind
listen
1.准备表
  fd_set readfds;
  FD_ZERO(&readfds);
2.添加要监控的文件描述符 
  FD_SET(listenfd,&reafds);
3.准备参数 
 maxfds = listenfd + 1;
 fd_set backfds; 
while (1)
{   
    backfds = readfds;
    int ret = select(maxfds,&backfds,NULL,NULL,NULL);
    if (ret > 0)
    {
        int i = 0;
        for (i = 0; i < maxfds;++i)
        {
            if (FD_ISSET(i,&backfds))
            {
                if (i == listenfd) //连接 
                {
                      connfd = accept();
                      //connfd 要被添加到 监控表
                      FD_SET(connfd,&readfds);
                      if (connfd + 1 > maxfds)
                         maxfds = connfd + 1;
                }else //负责与客户端通信 
                {
                //    i = ?//fd 此时就绪 
                    printf("buf = %s\n",buf);
                    if (strncmp(buf,"quit",4) == 0)
                    {
                        FD_CLR(i,&readfds); //清除对应的客户端的fd
                        close(i);    
                    }
                }
            }
        }
    }
}

//优化
int i = maxfds;
for (i = maxfds-1; i >= 0; --i)
{
    if (FD_ISSET(i,&readfds))
    {
        maxfds = i + 1;
    }
}

缺点
    1. 最大监听数受限:`FD_SETSIZE` 默认 1024(Linux)
    2. 每次调用需重置 fd_set:内核会修改集合,必须每次重新 `FD_SET`
    3. 用户态与内核态拷贝开销大
    4. 返回后仍需遍历所有 fd 才能知道哪个就绪
    5. 效率随 fd 数量增长下降明显

2.poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:  
    对文件描述符监控 
参数:
  @fds     
         struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

  events 事件:
        POLLIN    读 
        POLLOUT   写 
        POLLERR   错误 
  @nfds  表示要监控的文件描述符的数量 
  @timeout 时间值 
           -1 //阻塞 
           时间值 单位 是 ms (毫秒)
           0  //非阻塞 
          n(>0) //阻塞 n ms(毫秒)
返回值:
   成功 表示 就绪的数量 
       0 超时情况下表示 没有就绪实际
   失败 -1  

点对点聊天:

1.准备监控表 
  struct pollfd fds[10]; //监控表示 10个fd                                                                                           2.添加要监控的文件描述符   //点对点聊天 
  //两路io -- stdin / sockfd 
  int nfds = 0;
  fds[0].fd = 0;
  fds[0].events = POLLIN;
  nfds++;
  fds[1].fd = sockfd;
  fds[1].events = POLLIN;
  nfds++;
3.准备参数 
  while (1)
  {
      int ret = poll(fds,nfds,-1);
      if (ret > 0)
      {
        int i = 0;
        for (i = 0; i < nfds; ++i)
        {
            if(fds[i].revents&POLL_IN)
            {
                if (fds[i].fd == 0)
                {
                   fgets
                   send
                }else //sockfd 
                { 
                    recv
                    printf             
                }
            }
        }
      }
  }   

多路IO复用:

  listenfd = socket
  bind
  listen
1.准备表
  struct pollfd fds[10];
2.添加要监控的文件描述符 
  int nfds = 0;
  fds[0].fd = listenfd;
  fds[0].events = POLL_IN;
  nfds++;
  while (1)
  {   
    int ret = poll(fds,nfds,-1);
    if (ret > 0)
    {
        int i = 0;
        for (i = 0; i < nfds;++i)
        {
            if (fds[i].revents&POLL_IN))
            {
                if (fds[i].fd == listenfd) //连接 
                {
                      connfd = accept();
                      //connfd 要被添加到 监控表
                        fds[nfds].fd = connfd;
                        fds[nfds].events = POLL_IN;
                        nfds++;          
                }else //负责与客户端通信 
                {
                //    i = ?//fd 此时就绪 
                    recv(fds[i].fd,buf,sizeof(buf),0);
                    printf("buf = %s\n",buf);
                    if (strncmp(buf,"quit",4) == 0)
                    {
                        fds[i].fd = -1;  //-1 不是有效的文件描述符 
                        close(fds[i].fd);    
                    }
                }
            }
        }
    }
}

改进与不足:
相比 select 的改进
1. 无 1024 限制:只要系统允许打开足够多 fd
2. 无需重置集合:`events` 和 `revents` 分离
3. 更清晰的事件机制
4. 效率更高:仅遍历传入的数组,不遍历整个 fd 范围

仍存在的问题
1.每次调用仍需将整个 `fds[]` 拷贝到内核
2.返回后仍需遍历全部元素查找就绪 fd
3.时间复杂度仍是 O(n),连接数多时性能下降

3.epoll

相关函数:

1.epoll_create

int epoll_create(int size);
功能:
  创建一个epoll对象 
参数:
  @size 忽略,但是必须大于0 
返回值:
 成功 epoll对象的fd
 失败 -1 &&errno 

2. epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
   控制 epoll对象
参数:
  @epfd   epoll对象的fd
  @op    
     EPOLL_CTL_ADD   //添加
     EPOLL_CTL_MOD   //修改 
     EPOLL_CTL_DEL   //删除 
  @fd     
     //要关心的文件描述符   
  @event     
        typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;      /* Epoll events */
               epoll_data_t data;        /* User data variable */
           };

          
struct epoll_event  ev;
ev.events = EPOLLIN;  
ev.data.fd = 0; //stdin 

    EPOLLIN  //读 
    EPOLLOUT //写 
    EPOLLERR //出错
    EPOLLET  //边沿触发

返回值:
    成功 0
    失败 -1 &&errno

3.epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:
    监控对应的文件描述符 ,看是否有就绪
参数:
  @epfd  --epoll对象
  
  @events     ---保存就绪结果的 一个数组的首地址 
  @maxevents  ---数组的大小 

  struct epoll_event ret_ev[2];
  
 @timeout    ---超时的时间 
                时间单位,还是ms (毫秒数)
返回值:
  成功  就绪的数量 
  失败  -1 && errno被设置 

使用:

1.epoll_create //创建了一个epoll对象 ---- 监控的表 
  int epfd = epoll_create(2);
2.添加文件描述符 
//一个是 标准输入 0
//一个是 sockfd 
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = 0; //标准输入 
epoll_ctl(epfd,EPOLL_CTL_ADD, 0, &ev);
 
 int add_fd(int fd, int epfd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd; //标准输入 
    if ( epoll_ctl(epfd,EPOLL_CTL_ADD, fd, &ev))
    {
        perror("epoll_ctl add fail");
        return -1;
    }    
    
    return 0;
}
 
int del_fd(int fd, int epfd) //删除 
{
    //struct epoll_event ev;
    //ev.events = EPOLLIN;
    //ev.data.fd = fd; //标准输入 
    if ( epoll_ctl(epfd,EPOLL_CTL_DEL, fd, NULL))
    {
        perror("epoll_ctl add fail");
        return -1;
    }    
    
    return 0;
}
 
add_fd(0,epfd);//标准输入
add_fd(clifd,epfd);//socket

3.监控文件描述符 
while (1)
{
     struct epoll_event ret_ev[10];
     int ret = epoll_wait(epfd, ret_ev,3, -1);
     if (ret > 0)
     {
        int i = 0;
        for (i = 0; i < ret; ++i)
        {
            if (ret_ev[i].data.fd == 0)
            {
            }else  //socket
            {
              
            }
        }
        
     }
}

多路IO复用:

#include "../head.h"

int add_fd(int fd, int epfd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev))
    {
        perror("fail to epoll_ctl");
        return -1;
    }

    return 0;
}

int del_fd(int fd, int epfd)
{
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    if(epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL))
    {
        perror("fail to epoll_ctl");
        return -1;
    }

    return 0;
}

void set_nonblock(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    flags = flags | O_NONBLOCK;
    fcntl(fd, F_SETFL, flags);
    return ;
}

int main(void)
{
    int serfd = socket(AF_INET, SOCK_STREAM, 0);
    if(serfd < 0)
    {
        perror("fail to socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(50000);
    seraddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if(bind(serfd, (const struct sockaddr *)&seraddr, sizeof(seraddr)) < 0)
    {
        perror("fail to bind");
        return -1;
    }

    if(listen(serfd, 5) < 0)
    {
        perror("fail to listen");
        return -1;
    }

    struct sockaddr_in cliaddr;
    bzero(&cliaddr, sizeof(cliaddr));
    socklen_t len = sizeof(cliaddr);
    int connfd = 0;
    char buf[1024] = {0};

    int epfd = epoll_create(2);
    if(epfd < 0)
    {
        perror("fail to epoll");
        return -1;
    }

    add_fd(serfd, epfd);

    while(1)
    {
        struct epoll_event ret_ev[2];
        int ret = epoll_wait(epfd, ret_ev, 2, -1);
        int i = 0;
        if(ret > 0)
        {
            for(i =0; i < ret; i++)
            {
                if(ret_ev[i].data.fd == serfd)
                {
                   connfd = accept(serfd, (struct sockaddr *)&cliaddr, &len);
                   if(connfd < 0)
                   {
                    perror("fail to accept");
                    return -1;
                   }
                   printf("---client connect---\n");
                   set_nonblock(connfd);    //设置非阻塞
                   add_fd(connfd, epfd);
                }
                else
                {
                    int n = recv(ret_ev[i].data.fd, buf, 1, 0);
                    printf("n = 0, buf = %s\n",n, buf);
                    if(n < 0 && errno != EAGAIN)       //此时为接收完数据了没数据可接收了
                    {
                        perror("recv ");
                        del_fd(ret_ev[i].data.fd,epfd);
						close(ret_ev[i].data.fd);
                    }
                    if(n== 0 || 0 == strcmp(buf, ".quit"))  //此时为客户端退出了
                    {
                        del_fd(ret_ev[i].data.fd,epfd);
						close(ret_ev[i].data.fd);
                    }
                }
            }
        }
    }

    return 0;
}