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

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.多进程接收多客户端

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

cs 复制代码
#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.多线程接收多客户端

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

cs 复制代码
#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);

  1. 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复用:

cs 复制代码
#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;
}
相关推荐
半梦半醒*26 分钟前
playbook剧本
linux·运维·服务器·ssh·ansible·运维开发
跨境猫小妹1 小时前
亚马逊巴西战略升级:物流网络重构背后的生态革新与技术赋能之路
网络·重构·跨境电商·亚马逊
mit6.8241 小时前
[p2p-Magnet] 队列与处理器 | DHT路由表
网络·网络协议·p2p
北极光SD-WAN组网1 小时前
突破传统企业组网瓶颈:某科技公司智能组网服务项目深度解析
网络·科技
Ronin3052 小时前
【Linux系统】线程控制
linux·线程·线程控制
我也要当昏君2 小时前
5.2 I/O软件
java·网络·算法
qqxhb2 小时前
系统架构设计师备考第10天——网络技术-局域网&以太网
服务器·网络·系统架构·以太网·局域网
一只小鱼儿吖3 小时前
代理IP数量与IP池规模需求:分析与选择策略
网络·网络协议·tcp/ip
宇龙神3 小时前
Linux中的IP命令详解
linux
007php0074 小时前
Go 语言常用命令使用与总结
java·linux·服务器·前端·数据库·docker·容器