09_网络编程-网络模型
第五章 IO模型
5.1 I/O模型分类
IO模型是描述计算机如何处理输入/输出操作的方式。在网络编程和系统编程中,了解不同的IO模型对选择合适的编程方法和提高程序性能非常重要。常见的IO模型包括以下几种:
阻塞 与 运行
阻塞IO(Blocking I/O):
在阻塞IO模型中,应用程序调用IO函数时会被阻塞,直到IO操作完成。也就是说,程序在等待数据的传输期间无法执行其他操作。该模型简单易用,但在高并发情况下性能较差,因为每个IO操作都会占用一个线程或进程。
非阻塞IO(Non-blocking I/O):
非阻塞IO模型中,应用程序在调用IO函数时,如果数据未就绪,函数会立即返回,而不会阻塞程序的执行。应用程序需要不断地轮询(poll)IO函数以检查数据是否就绪。这种模型可以提高程序的响应能力,但轮询操作会消耗大量的CPU资源。
IO复用(I/O Multiplexing):
IO复用模型使用select
、poll
或epoll
等系统调用来监控多个文件描述符(fd),当其中一个或多个文件描述符就绪时,通知应用程序进行相应的IO操作。IO复用模型比非阻塞IO更加高效,因为它避免了轮询的高CPU占用问题,适用于处理大量并发连接的场景。
信号驱动IO(Signal-driven I/O):
在信号驱动IO模型中,应用程序可以为文件描述符设置信号驱动模式,并注册一个信号处理函数。当IO事件发生时,内核会通过信号通知应用程序。应用程序在收到信号后进行相应的IO操作。此模型减少了CPU轮询的开销,但信号处理需要格外小心,编程难度较大。
异步IO(Asynchronous I/O, AIO):
异步IO模型中,应用程序发起IO请求后立即返回,并且不等待IO操作完成。内核在完成IO操作后,会通知应用程序。异步IO允许程序在等待IO的过程中执行其他任务,因此可以最大化系统资源的利用效率。它适用于高并发的网络服务器或需要高实时性的数据处理场景。
各种IO模型的对比
IO模型 | 阻塞 | 非阻塞 | IO复用 | 信号驱动 | 异步IO |
---|---|---|---|---|---|
调用复杂度 | 简单 | 中等 | 较复杂 | 较复杂 | 复杂 |
系统开销 | 高 | 低 | 低 | 低 | 最低 |
并发性能 | 差 | 中等 | 高 | 高 | 最高 |
使用场景 | 简单 | 简单 | 多连接 | 高并发 | 高性能 |
后面两个 信号驱动IO 和 异步IO 都为我们后续的驱动 课程 内容 不在本次课程内讲述
5.2 阻塞I/O
是最基本和最常见的IO模型。它的特点是在进行IO操作时,程序会等待数据的读取或写入完成后才继续执行其他任务。在阻塞IO模型中,调用IO函数的进程或线程会被“阻塞”,即挂起,直到操作完成。这种模型简单易用,但在高并发环境下可能会导致性能问题。
创建三个管道文件 模拟阻塞通讯
创建管道文件的名称
mkfifo 文件名
示例程序如下
#include <head.h>
int main(int argc, char const *argv[])
{
printf("准备启动\n");
int fifo_1 = open( "fifo_1" , O_RDONLY);
printf("文件1启动成功\n");
int fifo_2 = open( "fifo_2" , O_RDONLY);
printf("文件2启动成功\n");
int fifo_3 = open( "fifo_3" , O_RDONLY);
printf("文件3启动成功\n");
char buf[128];
printf("全部文件启动成功\n");
while (1)
{
memset(buf , 0 , sizeof(buf));
read( fifo_1 , buf ,sizeof(buf) );
printf( "fifo_1 = %s\n" , buf );
memset(buf , 0 , sizeof(buf));
read( fifo_2 , buf ,sizeof(buf) );
printf( "fifo_2 = %s\n" , buf );
memset(buf , 0 , sizeof(buf));
read( fifo_3 , buf ,sizeof(buf) );
printf( "fifo_3 = %s\n" , buf );
}
return 0;
}
发送程序
#include <head.h>
#define FIFO "fifo_1"
int main(int argc, char const *argv[])
{
int fd_fifo = open( FIFO , O_WRONLY );
char buf[128];
while (1)
{
// 从键盘输入
printf("输入:");
scanf("%s" , buf);
// 写入到管道中
write( fd_fifo ,buf , strlen(buf) );
}
return 0;
}
5.3 非阻塞I/O
非阻塞模式 I/O 定义:将一个套接字设置为非阻塞模式时,相当于告诉系统内核:如果 I/O 操作不能立即完成,请不要让进程等待,而是马上返回一个错误。
Polling 机制:使用非阻塞模式的套接字时,需要通过一个循环不断测试文件描述符是否有数据可读,这个过程称为 “polling”。
CPU 资源浪费:频繁的 polling 操作会导致 CPU 资源的浪费,因为应用程序不断地占用 CPU 来检查 I/O 操作是否完成。
使用场景:这种模式在实际应用中不太常见。
在一些函数中 是有非阻塞 的选项的 但是大部分函数是没有非阻塞选项,所以需要使用 fcntl
函数
函数说明
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:用来控制文件描述符的状态
参数:
fd 要操作的 文件描述符
cmd
F_GETFL 获取文件描述符的状态 arg 被忽略
F_SETFL 设置文件描述符的状态 agr 是一个 int 类型
...: 可变参数
返回值:
如果cmd 为 F_GETFL 成功返回文件描述符标志位
如果cmd 为 F_SETFL 成功 返回 0
失败 返回 -1 并重置错误码
编写思路
1、需要通过 F_GETFL 获取原有的状态 给 flags
2、将 flags 或上 O_NONBLOCK (非阻塞状态)
3、然后通过 F_SETFL 写入你想要的flags
读端
写端
#include <head.h>
int main(int argc, char const *argv[])
{
printf("准备启动\n");
int fifo[3];
fifo[0] = open("fifo_1", O_RDONLY);
printf("文件1启动成功\n");
fifo[1] = open("fifo_2", O_RDONLY);
printf("文件2启动成功\n");
fifo[2] = open("fifo_3", O_RDONLY);
printf("文件3启动成功\n");
char buf[128];
printf("全部文件启动成功\n");
int read_sata;
int flags;
for (size_t i = 0; i < 3; i++)
{
flags = fcntl(fifo[i], F_GETFL); // 获取原始状态 并 返回给 flag
flags |= O_NONBLOCK; // 将 收到的 flag 增加一个非阻塞态
fcntl(fifo[i], F_SETFL, flags); // 将 flag 设定到我们当前的文件描述符中
}
while (1)
{
for (size_t i = 0; i < 3; i++)
{
memset(buf, 0, sizeof(buf));
read_sata = read(fifo[i], buf, sizeof(buf));
if (-1 != read_sata)
{
printf("fifo_%ld = %s\n",i , buf);
}
}
}
return 0;
}
5.4 多路复用 I/O
5.4.1 概念
IO多路复用也成为 IO 多路转接, 通过IO多路复用的方式可以同时检测多个文件描述符,并且这个检测的过程是阻塞的,一旦检测到某个文件描述符就绪,程序就会自动解除阻塞,之后就可以基于这些就绪的文件进行通讯。
通过这种方式可以在单进程/单线程的场景下面实现服务器并发,而我们常见的转接方式有:select
、poll
、epoll
。
5.4.2 select()
首先第一个介绍的是我们的 select
函数,select
函数是跨平台的函数,我们可以通过这个函数来检测我们的文件描述符状态。
他的原理就是检查我们提供文件描述符的读写缓冲区:
- 读缓冲区:检测内部有无数据,如果有数据则设定为就绪
- 写缓冲区:检测内部有无可以写写入的空间,有空间则设定改缓冲区就绪
- 读写异常:检测读写缓冲区是否有异常,有 则设定为就绪
函数原型
#include <sys/select.h>
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval * timeout);
参数:
nfds: 要监视的的最大文件描述符 + 1
readfds: 要监视 读 文件描述符 集合
不需要则 为 NULL
writefds: 要监视 写 文件描述符 集合
不需要则 为 NULL
exceptfds: 要监视 异常 文件描述符 集合
不需要则 为 NULL
timeout: 指定超时时间
NULL:永久阻塞 直到有文件描述符输入
等待固定时长:函数在固定时间没有检测到就绪 则直接接触阻塞 函数返回0
返回值:
大于0: 成功返回就绪文件描述符的总个数
等于-1: 函数调用失败
等于0: 超时 没有就绪的文件描述符
描述符集合
// 将文件描述符在集合中删除
void FD_CLR(int fd, fd_set *set);
0001 0010 1111
1、0x01 << 1024 - fd1
0001 0000 0000
2、取反
1110 1111 1111
3、取与运算
0001 0010 1111
1110 1111 1111
0000 0010 1111
// 判断文件描述符 是否在集合中 在集合中返回 非0 不在集合中 返回 0
int FD_ISSET(int fd, fd_set *set);
1、0x01 << 1024 - fd1
0001 0000 0000
2、取与
0001 0010 1111
0001 0000 0000
0001 0000 0000 | > 0 代表 文件描述符 在集合中
0000 0000 0000 | = 0 代表 文件描述符 不在集合中
// 将文件描述符添加到集合中
void FD_SET(int fd, fd_set *set);
1、0x01 << 1024 - fd1
0001 0000 0000
2、取或
0000 1000 0000
原始数据:0001 0010 1111
操作数据:0000 1000 0000
结果数据:0001 1010 1111
// 清空集合
void FD_ZERO(fd_set *set);
1、取与
原始数据:0001 0010 1111
操作数据:0000 0000 0000
结果数据:0000 0000 0000
select
的本质
// 类型原型
typedef struct
{
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
} fd_set;
// 参数拆分
typedef long int __fd_mask;
#define __FD_SETSIZE 1024
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
long __fds_bits[1024 / 64]
long __fds_bits[16]
1024 长度的比特数组
总共为 1024 比特
fd_set fd_s;
fd_s[1] = 1;
也就是说,在这个集合中 每一个位的位置 , 就代表我们的文件描述符。
当文件描述符所对应的 标志位 为 1 时 则代表 检测这个文件的状态
为 0 则反之
文件操作实例程序
#include <head.h>
int main(int argc, char const *argv[])
{
int fifo_1 = open("fifo_1", O_RDONLY);
int fifo_2 = open("fifo_2", O_RDONLY);
int fifo_3 = open("fifo_3", O_RDONLY);
char buf[128];
fd_set read_set; // 集合
fd_set read_set_temp; // 副本
int max_fds; // 最大值
int fds_num; // 就绪个数
// 清空集合
FD_ZERO(&read_set);
// 添加文件描述符
FD_SET(fifo_1, &read_set);
max_fds = (max_fds > fifo_1 ? max_fds : fifo_1);
FD_SET(fifo_2, &read_set);
max_fds = (max_fds > fifo_2 ? max_fds : fifo_2);
FD_SET(fifo_3, &read_set);
max_fds = (max_fds > fifo_3 ? max_fds : fifo_3);
while (1)
{
read_set_temp = read_set;
fds_num = select(max_fds + 1, &read_set_temp, NULL, NULL, NULL);
// 4 5 6
// 0 5 0
for (size_t i = 3; i < max_fds + 1; i++)
{
memset(buf, 0, sizeof(buf));
if (0 != FD_ISSET( i , &read_set_temp ))
{
read( i , buf, sizeof(buf));
printf("fifo = %s\n", buf);[]
}
}
}
return 0;
}
关于使用的问题
为啥只能监控 小于 FD_SETSIZE(1024) 的文件描述符
因为,fd_set只有 128 字节 只有 1024 比特
四个宏的本质
位置操作
晚上思考 四个宏 通过位操作如何实现
为什么 nfds 要传 max_fd + 1
nfds 获取方式 是通过 文件描述符 而文件描述符是从 0 开始计算
select 返回值 是什么含义 有什么用 怎么用
0 超时
-1 错误
>0 检测到多少个文件描述符就绪
减少循环遍历 FD_ISSET的 时间
使用select 的返回值 检测返回到多少个 文件描述符 在循环的过程中 执行一个 就 减减一次 当这个值 为0 的时候退出循环
循环中使用 select 为什么要每次重置集合
因为在 select 中 当文件就绪后, 集合中没有就绪的文件描述符 会被重置为0 ,下一次检测 就没有原先的文件描述符集合
在程序中创建一个母本 和 一个副本
5.4.3 poll()
==原理相同,需要同学们自行查阅资料学习==
5.4.4 epoll()
==原理相同,需要同学们自行查阅资料学习==
第六章 服务器模型
6.1 多线程并发服务器
需要解决的问题
1、如何做服务器转发
用全局变量做存储
用互斥锁做同步
2、如何检测TCP断开
int recv_size = recv(Sock_fd, buf_recv, sizeof(buf_recv), 0);
if (0 == recv_size) // 判定退出
示例程序
6.2 多进程并发服务器
需要解决的问题
1、如何防止僵尸进程
// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0); // 非阻塞等待子进程结束
}
// 处理僵尸进程
signal(SIGCHLD, handle_sigchld);
2、如何做服务器转发
很难解决
使用共享内存存储连接的用户列表
使用信号灯做同步
父进程 初始化 tcp
服务器套接字 3
等待子进程连接 4
子进程1 打开 套接字 4 3
子进程2 连接打开套接字 5 4 3
子进程3 连接打开套接字 6 5 4 3
实时操作系统
板载系统
32 stm32 ESP32
分线程
stm32 1M
64 A7 芯片
3、第一个连接的进程 无法向 第二个链接的进程发送信息
示例程序
main.c
#include <sys_head.h>
#include "tcp.h"
#include "list.h"
// 多进程 TCP 服务器
// 共享内存用定义
#define SHM_MAX_SIZE 16384
#define SHM_KEY 1234
#define SEM_KEY 12345
User_list *shm_init(void);
// 信号量加锁 进程的信号灯 P操作 获取 释放资源 进程的同步 或 互斥
void My_Sem_wait(int semid);
// 信号量解锁 V操作 创建资源
void My_Sem_signal(int semid);
// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig);
// 子进程处理函数
void handle_client(int Sock_fd, User_list *list, int semid);
int main(int argc, char const *argv[])
{
// 判断命令行参数
if (argc < 2)
{
printf("参数错误:%s ip port\n", argv[0]);
return -1;
}
/********************* 初始化TCP 服务器 *********************/
int Sock_server_fd = TCP_init(atoi(argv[1]));
/*********************** 初始化共享内存 **********************/
User_list *User_list_ptr = shm_init();
/*********************** 初始化信号灯集 **********************/
int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1); // 初始化信号量值为 1
/*********************** 注册处理僵尸进程 **********************/
signal(SIGCHLD, handle_sigchld);
/*********************** 声明收发缓冲区 **********************/
char buf_send[1024];
char buf_recv[1024];
while (1)
{
int Sock_client_fd = TCP_accept(Sock_server_fd);
// 创建进程
pid_t client_pid = fork();
if (0 == client_pid) // 子进程
{
// 关闭父进程的套接字 服务器套接字
close(Sock_server_fd); // 关闭父进程的套接字
// 子进程操作函数
handle_client(Sock_client_fd, User_list_ptr, semid);
shmdt(User_list_ptr);
exit(-1);
}
else if (0 < client_pid) // 父亲进程
{
My_Sem_wait(semid); // 加锁
// 将新的连接添加到链表中
User_list_ptr->data[User_list_ptr->len].Sock_fd = Sock_client_fd;
sprintf(buf_send, "%s|fd:%d|id:%d", "新用户", Sock_client_fd, User_list_ptr->len);
strcpy(User_list_ptr->data[User_list_ptr->len].name, buf_send);
send(Sock_client_fd, buf_send, strlen(buf_send), 0);
User_list_ptr->len++;
// 关闭子进程的文件描述符
// close(Sock_client_fd);
My_Sem_signal(semid); // 解锁
}
}
shmdt(User_list_ptr);
// 7、关闭套接字 close
close(Sock_server_fd);
return 0;
}
// 初始化共享内存
User_list *shm_init(void)
{
int shmid;
// 创建一个共享内存段,权限为 0666
shmid = shmget(SHM_KEY, SHM_MAX_SIZE, 0666 | IPC_CREAT);
if (shmid == -1)
{
perror("shmget 创建共享内存段失败");
exit(EXIT_FAILURE);
}
printf("共享内存段创建成功,ID: %d\n", shmid);
// 附加共享内存
User_list *user_list_ptr = (User_list *)shmat(shmid, NULL, 0);
if (user_list_ptr == (User_list *)-1)
{
perror("shmat failed");
exit(1);
}
// 打印指针的地址
printf("附加共享内存成功, 地址: %p\n", user_list_ptr);
// 清空共享内存
memset(user_list_ptr, 0, sizeof(User_list));
// 初始化顺序表
user_list_ptr->len = 0;
return user_list_ptr;
}
// 处理SIGCHLD信号,防止僵尸进程
void handle_sigchld(int sig)
{
while (waitpid(-1, NULL, WNOHANG) > 0)
; // 非阻塞等待子进程结束
}
// 信号量加锁
void My_Sem_wait(int semid)
{
struct sembuf Sem_b = {0, -1, 0};
semop(semid, &Sem_b, 1);
}
// 信号量解锁
void My_Sem_signal(int semid)
{
struct sembuf Sem_b = {0, 1, 0};
semop(semid, &Sem_b, 1);
}
// 子进程处理函数
void handle_client(int Sock_fd, User_list *list, int semid)
{
// 收发缓冲区
char buf_send[2048];
char buf_recv[1024];
// 计算当前进程所在顺序表的位置
My_Sem_wait(semid); // 加锁
int index = 0;
for (size_t i = 0; i < list->len; i++)
{
if (Sock_fd == list->data[i].Sock_fd)
{
index = i;
}
}
My_Sem_signal(semid); // 解锁
while (1)
{
memset(buf_send, 0, sizeof(buf_send));
memset(buf_recv, 0, sizeof(buf_recv));
int recv_size = recv(Sock_fd, buf_recv, sizeof(buf_recv), 0);
if (0 == recv_size) // 判定退出
{
// 从顺序表中删除
My_Sem_wait(semid); // 加锁
for (size_t i = index; i < list->len; i++)
{
list->data[index] = list->data[index + 1];
}
list->len--;
printf("退出成功....");
close(Sock_fd);
My_Sem_signal(semid); // 解锁
return;
}
// 修改名字
if (NULL != strstr(buf_recv, "set_name_"))
{
My_Sem_wait(semid); // 加锁
// 写入姓名
strcpy(list->data[index].name, buf_recv + 9);
sprintf(buf_send, "修改名称成功:%s", list->data[index].name);
send(Sock_fd, buf_send, strlen(buf_send), 0);
My_Sem_signal(semid); // 解锁
continue;
}
// 转发 群发
else
{
My_Sem_wait(semid); // 加锁
printf("开始群发\n");
// 组装字符串
sprintf(buf_send, "%s : %s", list->data[index].name, buf_recv);
// 遍历整个链表
printf("len = %d index = %d\n", list->len, index);
for (size_t i = 0; i < list->len; i++)
{
printf("name = [%s] fd = [%d]\n", list->data[i].name, list->data[i].Sock_fd);
}
// 发送给其他成员
for (size_t i = 0; i < list->len; i++)
{
if (Sock_fd != list->data[i].Sock_fd)
{
printf("aaaaa\n");
printf("发送的接收的fd = %d\n", list->data[i].Sock_fd);
int result = send(list->data[i].Sock_fd, buf_send, strlen(buf_send), 0);
if (result < 0)
{
perror("发送失败");
// 处理发送失败的情况(比如关闭连接、删除用户等)
}
}
}
My_Sem_signal(semid); // 解锁
}
}
}
list.h
#ifndef __LIST_H__
#define __LIST_H__
#define List_Max 50
// 声明 用户数据结构体
typedef struct tpc_Sock_fds
{
int Sock_fd;
char name[128];
} tpc_Sock_fds;
// 声明 用户顺序表
typedef struct User_list
{
tpc_Sock_fds data[List_Max];
int len;
}User_list;
#endif
tcp.c
#include "tcp.h"
#include <sys_head.h>
// 初始化函数
int TCP_init(int port)
{
int server_fd;
struct sockaddr_in address;
int opt = 1;
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0)
{
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)))
{
perror("setsockopt");
close(server_fd);
exit(EXIT_FAILURE);
}
// 配置地址结构
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(port);
// 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0)
{
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 监听套接字
if (listen(server_fd, 3) < 0)
{
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器启动成功 端口: %d...\n", port);
return server_fd;
}
int TCP_accept(int server_fd)
{
int new_socket;
struct sockaddr_in client_address;
socklen_t addrlen = sizeof(client_address);
// 阻塞等待客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&client_address, &addrlen)) < 0)
{
perror("accept");
exit(EXIT_FAILURE);
}
printf("新连接: socket fd = %d, ip : %s, port: %d\n", new_socket,
inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
return new_socket;
}
tcp.h
#ifndef __TCP_H__
#define __TCP_h__
int TCP_init(int port);
int TCP_accept(int server_fd);
#endif
6.3 多路IO复用
#include <sys_head.h>
// 默认IP
#define MY_IP "10.0.12.15"
// 定义一个全局变量的数组
typedef struct tpc_Sock_fds
{
int Sock_fd;
char name[128];
} tpc_Sock_fds;
#define MAX_LIST 20
#define SHM_ID 4
typedef struct tpc_sql_list
{
tpc_Sock_fds data[MAX_LIST];
int len;
} tpc_sql_list;
// 求集合最大值 宏函数
#define MAX_FDS(fd, Fd_Max) fd > Fd_Max ? fd : Fd_Max;
int main(int argc, char const *argv[])
{
/****************************************** 获取命令行参数 *****************************************/
// 判断命令行参数
if (argc < 2)
{
printf("参数错误:%s ip port\n", argv[0]);
return -1;
}
// 给定默认ip
char ip_str[128];
if (3 != argc)
{
// 写入默认IP
strcpy(ip_str, MY_IP);
}
else
{
strcpy(ip_str, argv[2]);
}
/****************************************** 获取命令行参数 *****************************************/
/***************************************** TCP服务器初始化 ****************************************/
// 1、创建套接字
int Sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == Sock_fd)
{
ERRLOG("socket erron");
return -1;
}
// 2、填充服务器网络信息结构体
struct sockaddr_in Sock_addr;
Sock_addr.sin_family = AF_INET; // 选择 IPV4
Sock_addr.sin_port = htons(atoi(argv[1])); // 给定端口 // atoi 将字符串 转换成数字
Sock_addr.sin_addr.s_addr = inet_addr(ip_str); // IP地址
socklen_t Sock_size = sizeof(Sock_addr);
// 3、将套接字与服务器的网络信息结构体绑定
if (-1 == bind(Sock_fd, (struct sockaddr *)&Sock_addr, Sock_size))
{
ERRLOG("bind err");
return -1;
}
// 4、将套接字设置城 监听状态
if (-1 == listen(Sock_fd, 20))
{
ERRLOG("Sock_fd err");
return -1;
}
printf("启动服务器\n");
/***************************************** TCP服务器初始化 ****************************************/
/***************************************** 初始化IO复用集合 ***************************************/
// 创建集合
fd_set Tcp_client_fds;
fd_set Tcp_client_fds_temp;
// 创建集合的最大值
int Max_fd;
// 清空母本
FD_ZERO(&Tcp_client_fds);
// 将监听的套接字给集合进行检测
FD_SET(Sock_fd, &Tcp_client_fds);
Max_fd = MAX_FDS(Sock_fd, Max_fd);
// 就绪描述符个数
int fds_num;
/***************************************** 初始化IO复用集合 ***************************************/
/***************************************** 初始化顺序表 ***************************************/
tpc_sql_list list;
list.len = 0;
/***************************************** 初始化顺序表 ***************************************/
/***************************************** 初始化缓冲区 ***************************************/
char buf[1024];
char buf_send[2048];
char buf_recv[1024];
memset(buf, 0, sizeof(buf));
memset(buf_send, 0, sizeof(buf_send));
memset(buf_recv, 0, sizeof(buf_recv));
/***************************************** 初始化缓冲区 ***************************************/
while (1)
{
memset(buf, 0, sizeof(buf));
memset(buf_send, 0, sizeof(buf_send));
memset(buf_recv, 0, sizeof(buf_recv));
// 副本获取母本数据
Tcp_client_fds_temp = Tcp_client_fds;
// 开始检测
fds_num = select(Max_fd + 1, &Tcp_client_fds_temp, NULL, NULL, NULL);
/*
说明: select 现在检测的是 服务器的TCP 套接字 和 客户端的TCP套接字
当 服务器的TCP 套接字 Sock_fd 就绪时 就代表有新的用户产生了链接
当 客户端的TCP 套接字 acc_fd 就绪时 就代表有用户发送了信息
*/
// 服务器的 TCP 套接字 Sock_fd 就绪
if (FD_ISSET(Sock_fd, &Tcp_client_fds_temp))
{
// 5、阻塞等待客户端连接 accept
int acc_fd = accept(Sock_fd, NULL, NULL);
if (-1 == acc_fd)
{
ERRLOG("accept err");
return -1;
}
// 检测是否超过最大连接数
if (list.len >= MAX_LIST)
{
printf("已经满了\n");
send(acc_fd, "已经满了", strlen("已经满了"), 0);
continue;
}
// 更新集合
FD_SET(acc_fd, &Tcp_client_fds);
Max_fd = MAX_FDS(acc_fd, Max_fd);
// 更新顺序表
list.data[list.len].Sock_fd = acc_fd;
sprintf(list.data[list.len].name, "新用户_00%d", list.len);
sprintf(buf_send, "连接成功 : %s", list.data[list.len].name);
send(acc_fd, buf_send, strlen(buf_send), 0);
printf("name [%s] fd = [%d]\n" , list.data[list.len].name , list.data[list.len].Sock_fd);
list.len++;
}
// 非连接 通讯
for (size_t i = 0; i < Max_fd + 1; i++)
{
// 判断那个文件描述符就绪
if (i != Sock_fd && FD_ISSET(i, &Tcp_client_fds_temp))
{
int index;
// 求当前的文件描述符 在 顺序表中的位置
for (size_t j = 0; j < MAX_LIST ; j++)
{
if (list.data[j].Sock_fd == i)
{
index = j;
break;
}
}
// 接收字符串
int recv_len = recv(list.data[index].Sock_fd, buf_recv, sizeof(buf_recv), 0);
// 客户端关闭了连接
if (0 == recv_len)
{
// 关闭了连接
printf("客户关闭了连接....\n");
// 从集合删除
FD_CLR(i, &Tcp_client_fds_temp);
close(i);
// 从顺序表删除
for (size_t j = index; j < MAX_LIST; j++)
{
list.data[j] = list.data[j];
}
list.len--;
continue;
}
// 客户端操作
// 修改名字
if (NULL != strstr(buf_recv, "set_name_"))
{
// 写入姓名
strcpy(list.data[index].name , buf_recv + 9);
// printf("改名字:%s index%d fd %d\n" , list.data[index].name , index , list.data[index].Sock_fd);
sprintf(buf, "修改名称成功:%s", list.data[index].name);
send( list.data[index].Sock_fd , buf, strlen(buf), 0);
continue;
}
// 转发 群发
// 发送给其他成员
// 组装字符串
sprintf(buf_send, "%s : %s", list.data[index].name, buf_recv);
for (size_t j = 0; j < list.len ; j++)
{
if (j != index)
{
printf("fd = [%d] name = [%s]\n", list.data[j].Sock_fd , list.data[j].name);
send(list.data[j].Sock_fd, buf_send , strlen(buf_send), 0);
}
}
}
}
}
// 7、关闭套接字 close
close(Sock_fd);
return 0;
}
- 感谢你赐予我前进的力量