8.网络编程

1.字节序

代码保存在 lesson31  byteorder.c

2.代码

(1) 定义一个联合变量

union{
    short value;
    char bytes[sizeof(short)]; // 将 short 转换为 byte 的容器
   }test;

(2)定义一个 short 类型的变量

test.value = 0x0102; // 定义一个包含两个字节的 value ;01 是一个字节,02 是一个字节

(3)将两个字节保存在 char 容器中

判断系统是如何保存的。其中 char 数组 [0] 是内存的低地址,[1] 是高地址

大端序

if((test.bytes[0]==1)&&(test.bytes[1]==2)) printf("大端字节序"); // 低地址位保存后面的值,高地址为保存前面的值

小端序

else if((test.bytes[0]==2)&&(test.bytes[1]==1)) printf("小端字节序\n"); // 低地址保存后面的值,高地址保存前面的值

3.代码演示

image-20211124144140362

1.1 字节序的转换

代码保存在 lesson31 bytetrans.c

没有运行出来

1.API

网络通信无论是大字节序还是小字节序现将这个字节序转换为大字节序然后再传给另一端,另一端在将大字节序转换为本主机读取的字节顺序

htons 转换端口,htonl 转换 IP

比如: ntoh将网络字节序转换为主机字节序

image-20211125112953635 image-20211125113013042

2.代码

(1)将一个数据进行小字节转换

// htons 转换端口
unsigned short a = 0x0102; // 定义一个无符号字节
printf("a:%x\n",a);
unsigned short b = htons(a); // 将主机抓换为小字节 short 
printf("b:%x\n",b);
image-20211125114826946

(2) IP 地址的转换

// htonl  转换IP
char buf[4] = {192, 168, 1, 100}; 
int num = *(int *)buf; // 先将其转换为 int 指针再对其进行取值
int sum = htonl(num); // 主机向网络转换为大端字
unsigned char *p = (char *)∑
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
printf("=======================\n");
image-20211125123411826

(3) 网络字节序向主机字节序的转换

// ntohl
unsigned char buf1[4] = {1, 1, 168, 192}; // 反序
int num1 = *(int *)buf1;
int sum1 = ntohl(num1);
unsigned char *p1 = (unsigned char *)&sum1;
printf("%d %d %d %d\n", *p1, *(p1+1), *(p1+2), *(p1+3));
image-20211125123444583

2.Socket 编程

2.1 常用的结构体变量

IPv4 的 Socket 结构体

struct sockaddr_in { 
  short int sin_family; // 地址族,代表是 ipv4 还是 ipv6
  unsigned short int sin_port; // 16 位的 TCP/UDP 端口号 
  struct in_addr sin_addr; // 32(ipv4) 位的 IP 地址
  unsigned char sin_zero[8]; // 一般不用
}
struct in_addr { 
  unsigned long s_addr;
}

2.2 地址转换的函数

代码保存在 lesson31 iptrans.c

1.API

image-20211125142556069

2.代码

将点分十进制转换为 IP

// 将点分十进制转换为 ip 字节序
char buf[] = "192.168.1.4";
unsigned int num=0;
inet_pton(AF_INET,buf,&num); // 1.地址族 2.要转换的字符串3.将结果写入这个字节序中 
unsigned char *p = (unsigned char *)#
printf("%d %d %d %d\n",*p,*(p+1),*(p+2),*(p+3));

将 IP 转换为点分十进制

// 将ip字节序转换为字符串
char ip [16] = ""; // 最后多的字符是 \0 
const char *str = inet_ntop(AF_INET,&num,ip,16);
printf("%s\n",str);

3.代码效果

先是点分十进制到 IP 地址的转换,再是 IP 地址到点分十进制的转换

image-20211125143110981

3.Socket 实战实现客户端服务端通信

其实 socket 的过程就是 server 得到 client 的 fd ,然后不断的向 client 的 fd 中读写数据。然后 client 也是不断的向 client fd 中读写数据

3.1 socket 的数据结构

这个结构体变量系统已经定义好了,只需要调用相关库,但是还是要了解一下变量的一些属性

struct sockaddr_in{
  short int sin_family; // 地址族:声明是 ipv4 还是 ipv6 协议
  unsigned short int sin_port; // 16 位的端口号
  struct in_addr sin_arrd; // 32位(ipv4) 的 IP 地址
  unsigned char sin_zero[8]; // 简单了解即可:sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
};

其中 in_addr 又是一种关于 ip 的数据机构,具体定义如下

sttuct in_addr{
  unsigned long s_addr; // 用于保存 IP 地址
};

3.2 服务端代码

1.创建一个 socket ,用于监听的套接字

socket 其实就是一个文件描述符,使用下面的方法创建一个 socket 。这个 socket 的作用用于监听服务端是否有连接,在服务器的后面还会调用一个 socket

image-20211126130305646
int lfd = socket(AF_INET,SOCK_STREAM,0); // 创建一个 socket
if(lfd==-1){ // 判断是否可以正常返回 socket 
  perror("socket");
  exit(-1);
}

2.使用 bind 方法,将 socket 绑定服务器 IP 地址和端口号

bind 的 API 如下所示:

image-20211126130508504

首先先要将服务端的 IP 地址和端口号实例化出来,也就是用刚才那个 sockaddr_in 的结构体变量进行定义。其次在将这个数据结构当做参数传递给 bind 方法

// 设置监听的 socket
struct sockaddr_in saddr;
saddr.sin_family = AF_INET; 
saddr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0 因为只是绑定不是主动发起连接,所以这个 socket 的地址是广播地址
saddr.sin_port = htons(9999); // 将主机端口号的字节序转换为网络可以看懂的端口号字节序
socklen_t ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr)); 
// 判断是否可以正常连接
if(ret==-1){
  perror("bind");
  exit(-1);
}

3.使用 listen 方法进行监听,监听的 socket 开始工作

image-20211126130836984
ret = listen(lfd,8); // 如果 backlog 设置太大会受到 SYN 的攻击(后面提到)
// 判断是否可以正常监听
if(ret==-1){
  perror("listen");
  exit(-1);
}

**4.accept 阻塞等待接收客户端连接,连接成功后会返回客户端的 socket **

客户端也是使用 socket 和服务端进行连接,如果连接成功了就会得到客户端的 socket ,后面再传输数据的时候就是用客户端的这个 socket 进行数据传输

image-20211126131221350
struct sockarrd_in clientaddr; // 定义客户端的 socket 
socklen_t len = sizeof(clientaddr);
int cfd = accept(lfd,(struct sockaddr *)&clientaddr,&len);
// 判断是否可以正常 accept
if(ret==-1){
  perror("accept");
  exit(-1);
}

通过上面的代码就可以得到客户端的 socket,下面可以将这个 socket 的基本信息进行打印

char clientIP[16]; // 用于保存 IP 地址
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clinetIP,sizeof(clientIP)); // 将网络端的 IP 地址转换成主机端的 IP
unsigned short clientPort = ntohs(clientaddr.sin_port); // 转换端接口号
printf("client ip is %s,port is %d\n",clientIP,clientPort); // 输出

5.通信

这里使用了一个 while 循环实现服务端一直监听客户端发送的消息,与此同时服务端还向客户端发送消息,这一步主要使用了以下两个函数。

image-20211126131434926

不管是读还是写套路都是一样的,先传入想要写入文件的 socket ,然后传入数据,最后传入数据变量的大小

char recvBuf[1024] = {0}; // 用于保存所读数据的 buff
while(1){
  int num = read(cfd,recvBuf,sizeof(recvBuf)); // 读取数据
  
  // 读取数据
  if(num==-1){ // 无法读取
    perror("red");
    exit(-1);
  }else if(num>0){ // 读取到信息
    print("recv client data:%s\n",recvBuf);
  }else if(num==0){ // 关闭连接 EOF 状态
    printf("client closed....");
    break;
  }
  
  // 向客户端发送数据
  char *data = "hello i am server";
  write(cfd,data,strlen(data));
}

6.关闭连接

数据传输完了别忘了把 socket 关闭

close(cfd);
close(lfd);

3.3 客户端代码

客户端的代码就比较简单了,这里就是主动向客户端连接

1.创建用于通讯的 socket

这个创建方法和服务器是一样的

int fd = socket(AF_INET,SOCK_STREAM,0);
// 判断是否可以正常 accept
if(ret==-1){
  perror("socket");
  exit(-1);
}

2.connect 连接服务器,需要制定服务端的 IP 和 端口

在连接服务器时先要将服务器的 socket 进行定义

image-20211126131353370
struct sockadder_in sevreraddr;
serveraddr.sin_family = AF_INET; // 地址族
inet_pton(AF_INET,"XXX.XXX.XXX.XXX",&serveraddr.sin_addr.s_addr); // IP 地址:这里 xxx 写的是要连接的服务器的 IP 地址
serveraddr.sin_port = htons(9999); // 端口号
int ret = connect(fd,(struct sockaddr *)&serveraddr,sizeof(serveraddr)); // 连接
if(ret==-1){
  perror("connect");
  exit(-1);
}

3.通信

char recvBuf[1024] = {0};
while(1){
  
  // 向服务端发送信息
  char *data = "hello i am client";
  write(fd,data,strlen(data)); // 向客户端发送数据
  sleep(1); // 休息一下
  
  // 读取从服务端接收到的数据
  int len = read(fd,recvBuf,sizeof(recvBuf));
  if(len==-1){
    perror("read");
    exit(-1);
  }else if(len>0){ // 将读取的信息打印
    printf("recv server data:%s\n",recvBuf);
  }else if(len==0){ // 断开链接
    printf("server closed...");
    break;
  }
}

4.关闭连接

closed(fd);

3.4 实现效果

1.先打开 server 的程序

在等待连接时他是阻塞的

image-20211128113006353

2.client 程序运行

客户端接收到了服务端发送的数据

image-20211128113113806

3.当客户端连接上服务器后

服务端接收到了客户端发送的数据,并将客户端的基本信息进行打印

image-20211128113149219

其中这个 backlog 好像还和 SYN 攻击有关

4.子进程实现高并发编程

代码保存在 lesson33 ,对应视频 4.21

4.1 服务端的实现

2.代码实现

(1)服务器端

  • 1.定义信号捕捉

捕捉到子进程结束的信号,并打印

定义信号

// 定义捕捉信号的相关函数
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉
sigaction(SIGCHLD, &act, NULL);

信号的处理函数

void recyleChild(int arg) {
  while(1) {
    int ret = waitpid(-1, NULL, WNOHANG);
    if(ret == -1) {
      // 所有的子进程都回收了
      break;
    }else if(ret == 0) {
      // 还有子进程活着
      break;
    } else if(ret > 0){
      // 被回收了
      printf("子进程 %d 被回收了\n", ret);
    }
  }
}
  • 2.关于 socket 的那些函数
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
  perror("socket");
  exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
  perror("bind");
  exit(-1);
}
// 监听
ret = listen(lfd, 128);
if(ret == -1) {
  perror("listen");
  exit(-1);
}
  • 3.不断的循环等待客户端的连接

首先要不断的 accept ,accept 成功之后再创建子进程进行处理

在子线程的处理机制当中,在将获得的这个 clientsocket 进行打印,与此同时添加 while 循环进行数据读取等操作

// 不断循环等待客户端连接
while(1) {

  struct sockaddr_in cliaddr;
  socklen_t len = sizeof(cliaddr);
  // 接受连接
  int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
  if(cfd == -1) {
    if(errno == EINTR) { // 当子进程断开链接后 accept 就终止
      continue;
    }
    perror("accept");
    exit(-1);
  }

  // 每一个连接进来,创建一个子进程跟客户端通信
  pid_t pid = fork(); // 高并发就是不断的开启子进程
  if(pid == 0) {
    // 子进程
    // 获取客户端的信息
    char cliIp[16];
    inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(cliaddr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    // 不断读取数据的 while 循环
    while(1) {
      int len = read(cfd, &recvBuf, sizeof(recvBuf)); // 读取客户端发送的消息
      if(len == -1) {
        perror("read");
        exit(-1);
      }else if(len > 0) {
        printf("recv client : %s\n", recvBuf);
      } else if(len == 0) {
        printf("client closed....\n");
        break;
      }
      write(cfd, recvBuf, strlen(recvBuf) + 1); // 向服务器发送消息
    }
    close(cfd);
    exit(0);    // 退出当前子进程
  }
}

(2)客服端实现

客户端就像往常一样,不断的创建 socket 向服务器发送消息

代码保存在 lesson33 ,client.c

3.实现效果

  • 1.服务端先开启进程等待客户端进行连接

服务端是阻塞状态

  • 2.开启客户端链接进程,并发送数据

打开多个窗口执行 client 程序,相当于打开多个客户端,实现并发访问服务器

image-20211130124010967
  • 3.服务器接收到数据
image-20211130124129449
  • 关闭客户端,服务器端打印断开链接

与此同时别的客户端也可以发送消息

image-20211130124321648

会出现的错误1: accept 报错

EINTR:当系统使用信号将进程杀死时会发送这个错误,导致该子进程的链接无效。所以继续下一次等待 accept 的循环

if(cfd==-1){
  if(errno == EINTR) continue;
  perror("accept");
  exit(-1);
}

5.子线程实现高并发编程

(1)服务端代码

子线程代码放在 lesson33 server 

  • 1.创建 sokect 以及绑定,监听等
// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
if(lfd == -1){
  perror("socket");
  exit(-1);
}

struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
  perror("bind");
  exit(-1);
}

// 监听
ret = listen(lfd, 128);
if(ret == -1) {
  perror("listen");
  exit(-1);
}
  • 2.while 循环监听 accept

首先要先使用 accept 方法得到 client 的 socket ,然后再为这个 socket 创建 thread ,这个 thread 就会处理 client socket 。下面是整体代码:

// 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
while(1) {

  struct sockaddr_in cliaddr;
  int len = sizeof(cliaddr);
  // 接受连接
  int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

  struct sockInfo * pinfo; 
  for(int i = 0; i < max; i++) {
    // 从这个数组中找到一个可以用的sockInfo元素
    if(sockinfos[i].fd == -1) {
      pinfo = &sockinfos[i];
      break;
    }
    if(i == max - 1) {
      sleep(1);
      i--;
    }
  }

  pinfo->fd = cfd;
  memcpy(&pinfo->addr, &cliaddr, len);

  // 创建子线程
  pthread_create(&pinfo->tid, NULL, working, pinfo);

  pthread_detach(pinfo->tid);
}

①关于 sockInfo 的结构体变量

这个结构体变量用于保存改进程所连接 client 的 IP 地址,端口号。所以将这些信息封装到一个结构体变量中

struct sockInfo {
  int fd; // 通信的文件描述符
  struct sockaddr_in addr;
  pthread_t tid;  // 线程号
};

② 对连接 socket 变量的初始化(在 while 循环之外操作)

首先将其设置为 128 个,然后将其中的值全部设置为 -1 ,代表这个 info 变量还没有被使用

struct sockInfo sockinfos[128];
// 初始化数据
int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
for(int i = 0; i < max; i++) {
  bzero(&sockinfos[i], sizeof(sockinfos[i]));
  sockinfos[i].fd = -1;
  sockinfos[i].tid = -1;
}

③在while 循环中判断有哪一个空的 info 信息可以使用

如果没有可以使用的 info 代表当前链接到了最大值

for(int i = 0; i < max; i++) {
  // 从这个数组中找到一个可以用的sockInfo元素
  if(sockinfos[i].fd == -1) {
    pinfo = &sockinfos[i];
    break;
  }
  if(i == max - 1) { // 到达了最大值,等待其他程序处理数据,然后释放一个 info 
    sleep(1);
    i--;
  }
}

④创建 info

将所连接的 client 信息放入 info 中

pinfo->fd = cfd;
memcpy(&pinfo->addr, &cliaddr, len);

⑤创建子线程

有了 accept 的 client ,有了 info 即当前服务器可以处理该请求,然后就可以创建子线程啦

// 创建子线程
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid); // 使用 detach 的方法系统自动回收资源,不用主线程回收

⑥关闭服务端的 socket

close(lfd);
  • 3.子线程的处理操作

子线程的操作和子进程的操作类似

这里做了一个控制连接的操作,一个服务器只能连接 128 个客户端

  • .每个子线程的处理函数

将 part4 中子进程处理的代码放在子线程中类似,只不过这里多加了一个对 info 的处理

void * working(void * arg) {
  // 子线程和客户端通信   cfd 客户端的信息 线程号
  // 获取客户端的信息
  struct sockInfo * pinfo = (struct sockInfo *)arg;

  char cliIp[16];
  inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
  unsigned short cliPort = ntohs(pinfo->addr.sin_port);
  printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

  // 接收客户端发来的数据
  char recvBuf[1024];
  while(1) {
    int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

    if(len == -1) {
      perror("read");
      exit(-1);
    }else if(len > 0) {
      printf("recv client : %s\n", recvBuf);
    } else if(len == 0) {
      printf("client closed....\n");
      break;
    }
    write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
  }
  close(pinfo->fd);
  return NULL;
}

(2)客户端操作

客户端代码没变,依旧用的子进程的

3.代码演示

最后实现效果同上一节

5.实现客户端的半关闭状态

为什么要实现半关闭状态:

当客户端向服务端发送完数据后就会执行 close(lfd) 指令,不能进行读写操作了。但是这个时候服务端还有数据没有发给客户端所以只能让 socket 保持半关闭状态即仅关闭写或者读的函数

1.API

使用这个函数进行 close 的作用是可以仅使得

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fo5doTmP-1644492921168)(https://gitee.com/xuboluo/images/raw/master/img/202202052238162.png)]

6.端口复用

6.1 TCP 的状态转换

程序保存在 lesson34 tcp_client.c 和 tcp_client.c

Linux 查看网络连接情况:

使用 netstat 查看网络链接状态

netstat -anp tcp 可以查看 socket 的连接状态

1.客户端和服务端同时运行程序

左边的 IP 地址代表目标 IP 地址。右边的地址是源 IP 地址。所以右侧是端口号是 9999 的代码可以判断出为服务端

image-20211201125538905

2.先将服务端断开链接

image-20211201130151110

客户端还没有立刻退出

3.Client 断开链接

可以看到客户端是 TIME_WAIT,而且在一分钟 2MSL 之内 ./sever 进程是无法启动的

image-20211201130447256

image-20211201132724459

6.2 setsockopt 端口复用函数

程序保存在 lesson34 tcp_server.c 和 tcp_client.c

1.API

课本 《Unix编程》P165 level 的参数值

用途:

客户端在断开连接后会有 2ML 的时间服务器是不能启动的,说端口被占用。这个时候别的程序就不能对其进行访问,端口复用可以在这 2ML 的内让其他客户端连接

程序突然退出没有释放端口

image-20211201131754410

setsockopt 方法其实是对 socket 进行设置

设置端口复用的时机:在服务器绑定端口之前。也就是 bind 函数定义之前

2.代码实现

给服务器在 bind 方法之前添加端口复用

int optval = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
// 绑定
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if(ret == -1) {
  perror("bind");
  return -1;
}

3.代码实现

client 程序输入一个 hello ,服务器接收到这个 hello 并转换成大写。

当客户端的进程关闭后再次开启服务端,就不会再受到 2MSL 的影响

image-20211201132506889 image-20211201132640286

7.IO 多路复用

7.1 相关定义

I/O 多路复用使得程序能同时监听多个文件描述符发出的请求(是什么),能够提高程序的性能(为什么),Linux 下实现 I/O 多路复用的系统调用主要有select、poll 和 epoll(怎么做)。

以往时候:遍历每个连接的客户端 socket 的读缓冲区是否有数据。然后连接多个 socket 的方法是服务器不断的创建子线程与子进程用于处理 socket 请求

现在:同时知道有多少个数据给发来了 socket 通信消息,交由内核进行处理

7.2 常见的 IO 模型

7.2.1 阻塞等待 B(blocking)IO

1.理论

一次连接一个客户端,在客户端1还没有数据发来的时候服务器的 socket 是阻塞状态,一直等着客户端发来消息

好处:不占用 CPU 的使用。因为当该等待进程在等待过程中 CPU 是可以执行其他程序的、

缺点:同一时刻只能处理一个操作,效率低。这个服务器要一直等着这个客户端发送来数据

解决:多线程 or 多进程解决。同时监听多个客户端

上面解决方法还有缺点:①线程或者进程会消耗资源②线程或者进程调度消耗 CPU 资源

2.实现原理(多线程)

因为 read 和 write 方法在监听当前客户端时会有阻塞状态,所以需要再创建新的进程监听别的客户端的信息

image-20211230233429797

7.2.2 非阻塞,忙轮询 NIO 模型

1.理论

不用一直阻塞监听 read 函数中是否有数据。只是不断的去看看 read 中是否有数据

优点:提高了程序的执行效率。因为当前服务端的程序可以执行其他的函数

缺点:需要占用更多的 CPU 和消耗更多的资源 ,因为要不断轮询。假如说有1w个数据进行连接,要将这 1w 个数据全部遍历一遍查看是否有数据

2.实现原理

image-20211214103244087

7.2.3 IO 多路转接技术

1.理论

相当于搭建了一个站点,这个站点帮忙接收客户端所有的读写消息。这样就不用主线程一直监听,而是让站点监听。而这个站点就是内核,内核一直帮服务器监听有哪些客户端发送了数据,并汇总起来通知客户端

(1)select / poll

内核只通知服务器有几个客户端发送了请求,但是具体哪个客户端发送了请求还需要服务器自己遍历

(2)epoll

内核不仅通知服务器有哪些客户端发送了请求,还会通知服务器是哪些客户端发送的请求

2.多路复用的优点

(1)相⽐基于进程的模型给程序员更多的程序⾏为控制。
(2)IO多路复⽤只需要⼀个进程就可以处理多个事件,单个进程内数据共享变得容易,调试也更容易。
(3)因为在单⼀的进程上下⽂当中,所以不会有多进程多线程模型的切换开销。

3.多路复用的缺点

(1)不能充分利⽤多核处理器。

7.2 Select 函数

7.2.1 Select 的基本原理

1.基本原理

(1)构造一个专门监管客户端 sokect 连接情况的 fd_set ,将要监听的文件描述符添加到该列表中

(2)调用一个系统函数,监听 fd_set 的文件描述符,直到这些文件描述符中有一个或者多个 socket 进行 IO 操作时该函数才返回。

​ a.该函数是阻塞的

​ b.对于文件描述符列表的检测操作是内核完成的

(3)在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作

2.举例

(1)所有和服务器进行连接的客户端 socket 都保存在 fd_set 中。fd_set 大小默认是 1024 ,但是有时候我们只需要监听 100 个 socket ,所以需要设置在 fd_set 中监听的最大描述符的位置,这个位置是开区间,所以需要 +1 。

如果 select 监听的 fd 超过了 1024 会出什么错误

(2)现在需要监听 3,4,100,101 这三个 fd 。程序先将 set 从用户态拷贝到内核态。内核检查需要检查的 fd 哪些是发送 read 或者 write 请求的,发现 100 ,101 发送了请求。于是将这两个标志位设为 1 ,其余设为 0 ,然后再发送给用户态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-viwwh3FV-1644492921169)(/Users/xuguagua/Documents/typora_image/image-20211203135521629.png)]

3.用户态再遍历哪些 fd 发生了改变于是就对这些 fd 进行处理

image-20211203135719412

7.2.2 select —检测哪些文件描述符有数据

1.API

read 和 write 的集合是相反的,read 是有读数据的 socket 位置为 1 ,write 是为 0 的时候才进行写操作

这个 set 是一个指针,也就是内核可以控制主进程也可以控制

image-20211203134245258

设置集合中文件描述符标志位的函数:

image-20211203134321564

2.代码

创建一个服务器不断的监听是否有客户端连接进来。然后再创建一个客户端不断的连接服务端。

(1) 服务端监听 socket 代码

// 创建socket
int lfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;

// 绑定
bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

// 监听
listen(lfd, 8);

(2)创建一个 fd_set 的集合

fd_set 中主要存放需要检测的文件描述符

// 创建一个fd_set的集合,存放的是需要检测的文件描述符
fd_set rdset, tmp; // tmp 是内核态更改的 fd_set 
FD_ZERO(&rdset);
FD_SET(lfd, &rdset); // 将服务器的 socket 放入 fd_set 
int maxfd = lfd; // 一开始还没有客户端的文件描述符连接,所以最大文件描述符让其等于服务器的就行

(3)不断的 while 循环监听文件描述符变化

服务器的内核区不断的监听客户端文件描述符中的改变

最外层是 while 循环

a. 没有 socket 发生改变

tmp = rdset; // 内核处理的文件描述符

int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL); // 调用select系统函数,让内核帮检测哪些文件描述符有数据
if(ret == -1) { // 检测失败
  perror("select");
  exit(-1);
} else if(ret == 0) { // 没有文件描述符发生变化
  continue;
}

b. Socket 发生变化

else if(ret > 0) { // 改变
  // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
  if(FD_ISSET(lfd, &tmp)) { // 判断服务器监听描述符是否有变化
    // 表示有新的客户端连接进来了
    struct sockaddr_in cliaddr;
    int len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    // 将新的文件描述符加入到集合中
    FD_SET(cfd, &rdset); // 添加的时候是在 rdset 中进行添加,只有处理 socket 请求时才在 tmp 中进行处理

    // 更新最大的文件描述符
    maxfd = maxfd > cfd ? maxfd : cfd;
  }
  // for 循环不断监听 fd_set 中哪个 socket 发生了变化
  for(int i = lfd + 1; i <= maxfd; i++) {
    if(FD_ISSET(i, &tmp)) { // 如果返回 1 说明检测到了变化
      // 说明这个文件描述符对应的客户端发来了数据
      char buf[1024] = {0};
      int len = read(i, buf, sizeof(buf));
      if(len == -1) {
        perror("read");
        exit(-1);
      } else if(len == 0) {
        printf("client closed...\n");
        close(i);
        FD_CLR(i, &rdset);
      } else if(len > 0) {
        printf("read buf = %s\n", buf);
        write(i, buf, strlen(buf) + 1); // 读取完数据后向客户端写数据回复
      }
    }
  }

}

(4) 客户端代码

客户端就是连接主机向主机发送数据,代码写过很多遍了

7.2.3 Select 的缺点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A6Hm6kor-1644492921170)(https://gitee.com/xuboluo/images/raw/master/img/202202052235860.png)]

7.3 poll 函数

程序保存在 lesson35 client.c 和 poll.c

7.3.1 poll 的代码实现

1.API

poll 最终改进了 select 的 34 缺点

image-20211205164458278 image-20211205164617241

2.代码实现

服务器端代码实现 , poll 的代码和 select 的代码非常像,前面都是先创建服务器监听的 socket ,然后使用 while 循环不断监听 accept 进来的请求

(1) 创建文件描述符数组

将其 id 进行初始化为 -1 ,并且为这个 client 的 socket 设置监听事件

// 初始化检测的文件描述符数组
struct pollfd fds[1024]; // 可以初始化多个文件描述符
for(int i = 0; i < 1024; i++) {
  fds[i].fd = -1;
  fds[i].events = POLLIN; // 监听 client socket 的读事件
}
fds[0].fd = lfd; // 集合第一个元素是服务器监听的 socket
int nfds = 0; // 最大下标

(2)while 循环不断监听 client 的连接并对时间进行处理

下面所有代码都是在 while 循环中定义的

a. 没有 socket 连接

// 调用poll系统函数,让内核帮检测哪些文件描述符有数据
int ret = poll(fds, nfds + 1, -1);
if(ret == -1) {
  perror("poll");
  exit(-1);
} else if(ret == 0) {
  continue;

b.监听有新的 clinet socket 进行连接

} else if(ret > 0) {
  // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
  if(fds[0].revents & POLLIN) { // 这里对事件的监听不能用 == ,因为监听写入事件只是其中一个事件,还有别的事件
    // 表示有新的客户端连接进来了
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

    // 将新的文件描述符加入到集合中
    for(int i = 1; i < 1024; i++) {
      if(fds[i].fd == -1) { // 还能够加入
        fds[i].fd = cfd;
        fds[i].events = POLLIN;
        break;
      }
    }
    // 更新最大的文件描述符的索引
    nfds = nfds > cfd ? nfds : cfd;
  }

c.处理新连接的 client socket

读取 client 的写入操作

for(int i = 1; i <= nfds; i++) {
  if(fds[i].revents & POLLIN) {
    // 说明这个文件描述符对应的客户端发来了数据
    char buf[1024] = {0};
    int len = read(fds[i].fd, buf, sizeof(buf));
    if(len == -1) {
      perror("read");
      exit(-1);
    } else if(len == 0) {
      printf("client closed...\n");
      close(fds[i].fd);
      fds[i].fd = -1;
    } else if(len > 0) {
      printf("read buf = %s\n", buf);
      write(fds[i].fd, buf, strlen(buf) + 1);
    }
  }
}

(3)客户端连接代码

客户端连接代码就是客户端不断的向服务器发送消息

3.代码效果

服务端启动后不断接收客户端发来的数据,而且是在一个线程和一个进程情况下接收数据

image-20211205161407762

开启多个客户端接收服务端的数据

image-20211205161654170

7.3.2 poll 的缺点

每一次还是要对集合内的 socket 进行一一遍历

7.4 epoll

程序保存在 lesson35 client.c 和 epoll.c

7.4.1 epoll 基础

1.定义

epoll_creat 是在内核中创建了一个数据结构:eventpoll ,返回一个文件描述符(站点),这样可以直接对内核进行操作

相关数据结构:

rbr:直接在内核中创建一个红黑树的数据结构用于存放需要检测的 socket

rdlist:创建一个列表用于判断哪些 socket 发生了变化

epoll_event:需要监听的事件

image-20211205164717820

改善了以下问题:

(1)直接在内核态处理数据,不经过用户态

(2)使用红黑树进行客户端 socket 的遍历速度加快

(3)只返回发生状态改变的 socket ,不像 select 要把所有 set 中的 fd 进行返回

2.epool中 ET 和 LT的区别与实现原理

(1)边缘触发 edge trigger

当被监控的 Socket 描述符上有可读事件发⽣时,服务器只会从 epoll_wait中苏醒⼀次,即使进程没有调⽤read 函数从内核读取数据,也依然只苏醒⼀次,因此我们程序要保证⼀次性将内核缓冲区的数据读取完,只有第⼀次满⾜条件的时候才触发,之后就不会再传递同样的事件了。

输入 123 后点回车操作,输出 hello word 触发一次

(2)水平触发 level trigger

当被监控的 Socket 上有可读事件发⽣时,服务器不断地从 epoll_wait中苏醒,直到内核缓冲区数据被 read函数读完才结束,⽬的是告诉我们有数据,只要满⾜事件的条件,⽐如内核中有数据需要读,就⼀直不断地把这个事件传递给⽤户。

输入 123(缓冲区没满),一直输出 hello world

举例理解 ET,LT

3.one_shot 事件

(1)什么是 one_shot 事件

即使可以使用 ET 模式,一个 socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个 socket 的局面。一个socket连接在任一时刻都只被一个线程处理,可以使用 epoll 的 EPOLLONESHOT 事件实现。

(2)one_shot 如何避免上述问题

对于注册了 EPOLLONESHOT 事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的。但反过来思考,注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket。

7.4.2 epoll 代码实现

1.API

(1)关于 epoll 的结构体变量

a.epoll_data:所检测到的 socket 的信息

b.所要监听的 socket 的事件(一般创建一个数组用于监听多个 socket )

image-20211205170615214

events:需要监听什么事件

epoll_data_t : 监听谁,比如服务器的 lisiten socket

(2) 创建 epoll 实例(站点)

image-20211205170334239

(2)如何对 epoll 进行管理

定义 epoll 为我们做什么

image-20211205170721007

(3)检测函数

将空的数组传入 ,epoll 返回发生改变的 socket 。是一个阻塞的函数,如果没有客户端去连接则会一直阻塞在那,不往下执行

image-20211205170757129

2.服务器代码

(1)创建一个站点

// 调用epoll_create()创建一个epoll实例
int epfd = epoll_create(100); // 最后这个 100 是随便写的

(2)对监听的文件描述符进行检测

检测是否有与其连接

// 将监听的文件描述符相关的检测信息添加到epoll实例中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;

(3)使用 epoll 进行控制

// 添加一个 epoll 的控制事件
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); // epoll 的文件控制符,添加一个黄牛,服务端的 socket ,所有控制的client 

(4)创建多个 epoll 机构题变量

用于存放发生改变的 socket ,用于 epoll 的返回

struct epoll_event epevs[1024]; 

(5)监听不断改变的 socket

以下代码在 while 循环中实现,首先先使用 wait 方法不断监听发生的改变

int ret = epoll_wait(epfd, epevs, 1024, -1); // 检测数据是否发生改变,将所有发生改变的数据放到 epevs 中,并让其阻塞
if(ret == -1) { // 调用失败
  perror("epoll_wait");
  exit(-1);
}

printf("ret = %d\n", ret);

使用 for 循环不断遍历连接

(6)监听 server 的连接事件

for(int i = 0; i < ret; i++) { // 不断的循环遍历改变的文件描述符

  int curfd = epevs[i].data.fd; // 得到发生改变的文件描述符

  if(curfd == lfd) { // 服务器监听的描述符发生改变
    // 连接
    struct sockaddr_in cliaddr;
    socklen_t len = sizeof(cliaddr);
    int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
    // 为 accept 的 client socket 创建 event 
    epev.events = EPOLLIN; // 监听 client 的读事件
    epev.data.fd = cfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); // 将新的 client socket 添加到监听队列中
  } else {
    if(epevs[i].events & EPOLLOUT) { // 监听写事件
      continue;
    }  

(7)监听 client 的写事件

// 有数据到达,需要通信
char buf[1024] = {0};
int len = read(curfd, buf, sizeof(buf));
if(len == -1) {
  perror("read");
  exit(-1);
} else if(len == 0) {
  printf("client closed...\n");
  epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
  close(curfd);
} else if(len > 0) {
  printf("read buf = %s\n", buf);
  write(curfd, buf, strlen(buf) + 1);
}

(8)关闭文件描述符

close(lfd);
close(epfd);

3.代码演示

客户端的代码和代码演示同前面一样

7.4.3 epoll 的三种模式

1.LT(level trigger) 电平触发模式

epoll_wait检测到事件发生后通知了主线程 -> 主线程可以不立即处理该事件。下一次调用epoll_wait时,还会再次向应用程序通知此事件,直到该事件被处理

2.ET(Edge Trigger)边沿触发模

epoll_wait检测到事件发生后通知了应用程序 ->应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。

产生的问题:

即使可以使用 ET 模式,一个socket 上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程在读取完某个 socket 上的数据后开始处理这些数据,而在数据的处理过程中该socket 上又有新数据可读(EPOLLIN 再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个 socket 的局面。一个socket连接在任一时刻都只被一个线程处理,可以使用 epoll 的 EPOLLONESHOT 事件实现。

3.EPOLLONESHOT 事件

EPOLLONESHOT事件:保证一个socket连接在任一时刻只被一个线程处理

操作系统最多触发其上注册的一个可读、可写或者异常事件,除非我们使用 epoll_ctl 函数重置该文件描述符上注册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个 socket 时,其他线程是不可能有机会操作该 socket 的

注册了 EPOLLONESHOT 事件的 socket 一旦被某个线程处理完毕, 该线程就应该立即重置这个 socket 上的 EPOLLONESHOT 事件,以确保这个 socket 下一次可读时,其 EPOLLIN 事件能被触发,进而让其他工作线程有机会继续处理这个 socket

8.UDP 通信

代码保存在 lesson37 udp_server.c & udp_client.c 中

8.1 UDP 通信过程

UDP 连接是不可靠连接,所以不需要客户端实现监听的 socket 。客户端的 socket 会和客户端直接建立连接

8.2 UDP 连接代码

1.API

UDP 和 TCP 都是使用 socket 进行连接,只不过二者的 read 和 write 函数不太一样

发送数据的函数:

image-20211209223215865

接收数据的函数:image-20211209223400134

2.代码

(1)服务器代码

服务器端:1)创建套接字create;2)绑定端⼝号bind;3)接收/发送消息recvfrom/sendto;4)关闭套接字

因为创建的是不可靠连接所以就没有 accpet 的那些过程,直接传输数据

① 绑定 socket

// 1.创建一个通信的socket 文件描述符
int fd = socket(PF_INET, SOCK_DGRAM, 0); // 使用什么协议,以什么样的方式进行数据传输

if(fd == -1) {
  perror("socket");
  exit(-1);
}

// 2.创建一个 socket 实例
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;

// 3.绑定
int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
  perror("bind");
  exit(-1);
}

②监听数据

// 4.通信
while(1) {
  char recvbuf[128];
  char ipbuf[16];

  struct sockaddr_in cliaddr;
  socklen_t len = sizeof(cliaddr);

  // 接收数据
  int num = recvfrom(fd, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&cliaddr, &len);

  printf("client IP : %s, Port : %d\n",
         inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ipbuf, sizeof(ipbuf)),
         ntohs(cliaddr.sin_port));

  printf("client say : %s\n", recvbuf);

  // 发送数据
  sendto(fd, recvbuf, strlen(recvbuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));

}

(2)客户端

和服务端差不多,但是没有 bind 那一部分

客户端:1)创建套接字create;2)发送/接收消息sendto/recvfrom;3)关闭套接字

// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
  perror("socket");
  exit(-1);
}   

// 服务器的地址信息
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr);
int num = 0;

// 3.通信
while(1) {
  // 发送数据
  char sendBuf[128];
  sprintf(sendBuf, "hello , i am client %d \n", num++);
  sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&saddr, sizeof(saddr));

  // 接收数据
  int num = recvfrom(fd, sendBuf, sizeof(sendBuf), 0, NULL, NULL);
  printf("server say : %s\n", sendBuf);
  sleep(1);
}
close(fd);

3.代码效果

两者之间可以相互发送数据

运行服务器

image-20211210102136795

运行客户端

9.广播

9.1 广播基础

详见 Xmind

9.2 广播代码

1.API

image-20211210105125558

2.代码

(1)服务器

服务器的关键代码就是给设置一个广播的属性,这个方法也用于端口多路复用中。*这里就不需要 bind 连接,因为广播是向全部的子网主机发送消息。下面是关键代码:

// 2.设置广播属性
int op = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));

完整代码:

// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
  perror("socket");
  exit(-1);
}   

// 2.设置广播属性
int op = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof(op));

// 3.创建一个广播的地址
struct sockaddr_in cliaddr;
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(9999);
inet_pton(AF_INET, "10.22.200.91", &cliaddr.sin_addr.s_addr);

// 3.通信
int num = 0;
while(1) {

  char sendBuf[128];
  sprintf(sendBuf, "hello, client....%d\n", num++);
  // 发送数据
  sendto(fd, sendBuf, strlen(sendBuf) + 1, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
  printf("广播的数据:%s\n", sendBuf);
  sleep(1);
}

(2)客户端

在客户端需要绑定本地的 IP 和端口,接收广播发送给客户端的数据

// 1.创建一个通信的socket
int fd = socket(PF_INET, SOCK_DGRAM, 0);
if(fd == -1) {
  perror("socket");
  exit(-1);
}   

struct in_addr in;

// 2.客户端绑定本地的IP和端口
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.s_addr = INADDR_ANY;

int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
  perror("bind");
  exit(-1);
}

// 3.通信
while(1) {
  char buf[128];
  // 接收数据
  int num = recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
  printf("server say : %s\n", buf);
}

3.代码演示

(1)服务器

服务器广播发送数据

image-20211210111305410

(2)客户端

客户端是从中间的部分才接收到的数据

image-20211210111401832

10.组播

10.1 组播基础

详见 Xmind

组播会先将一个主机地址设置成一个组播地址。然后再让相应的客户端加入组播组,最后服务器通过组播的地址向组播组发送数据

image-20211210135828729

10.2 组播代码实现

1.API

image-20211210135414272

数据结构:

image-20211210135452706

2.代码

(1)服务器

关键代码:设置多播的相关属性

// 2.设置多播的属性,设置外出接口
struct in_addr imr_multiaddr;
// 初始化多播地址
inet_pton(AF_INET, "239.0.0.10", &imr_multiaddr.s_addr);
setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof(imr_multiaddr));

(2)客户端

关键代码:绑定相应的广播组

int ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr)); // 客户端绑定相应的广播组

将该端添加到组播组

// 构建组播的数据结构
struct ip_mreq op;
inet_pton(AF_INET, "10.22.200.91", &op.imr_multiaddr.s_addr);
op.imr_interface.s_addr = INADDR_ANY;

// 加入到多播组
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof(op));

3.代码演示

组播和广播的区别就在于组播可以向不同子网的 IP 地址发送消息

(1)服务器

image-20211210140920973

(2)客户端

image-20211210140849391

11.本地套接字

代码保存在 lesson37 ipc_server.c & ipc_client.c

11.1 本地套接字基础

本地套接字的作用:本地的进程间通信

有关系的进程间的通信

没有关系的进程间的通信

本地套接字实现流程和网络套接字类似,一般采用TCP的通信流程。

socket 的类型

image-20211210143136731

本地套接字实质:

本地套接字实质就是两个进程间创建两个文件,这两个文件是两个缓冲区向缓冲区内写和读数据

image-20211210144045085

11.2 本地套接字通信代码(未整理完,以后用到的话再回顾)

1.整体流程

(1)服务器

image-20211210143405257

(2)客户端

image-20211210143441609

(3)套接字的数据结构

image-20211210143529984

2.代码

客户端和服务器代码差不多

(1)服务器

删除之前创建的套接字文件

如果不删除创建的 socket 文件下次启动服务器重复创建会报错

unlink("server.sock");

创建套接字

// 1.创建监听的套接字
int lfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(lfd == -1) {
  perror("socket");
  exit(-1);
}

(2)客户端

删除之前创建的套接字文件

如果不删除创建的 socket 文件下次启动客户端重复创建会报错

unlink("client.sock");

创建套接字

// 1.创建套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM, 0);
if(cfd == -1) {
  perror("socket");
  exit(-1);
}

绑定本地套接字文件

// 2.绑定本地套接字文件
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy(addr.sun_path, "client.sock");
int ret = bind(cfd, (struct sockaddr *)&addr, sizeof(addr));
if(ret == -1) {
  perror("bind");
  exit(-1);
}

3.代码效果

服务器端进程接收到客户端进程发送的数据

image-20211210145752096

客户端进程向服务器进程发送数据

image-20211210145832101

12.两种事件处理模式

服务器程序通常需要处理三类事件:I/O 事件、信号及定时事件。有两种高效的事件处理模:Reactor 和 Proactor,同步 I/O 模型通常用于实现 Reactor 模式,异步 I/O 模型通常用于实现 Proactor 模式。

12.1 Reactor模式

1.定义
要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

2.如何实现

使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
(1)主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
(2)主线程调用 epoll_wait 等待 socket 上有数据可读。
(3)当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
(4)睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件。
(5)当主线程调用 epoll_wait 等待 socket 可写。
(6)当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
(6)睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。

image-20220102213031287

12.2 Proactor

1.定义
Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。

2.工作流程
使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程:
(1)主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例)。
(2)主线程继续处理其他逻辑。
(3) 当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据
已经可用。
(4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
(5) 主线程继续处理其他逻辑。
(6) 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
(7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。

image-20220102214646614

3.二者区别区别

Reactor:数据读取操作由子线程完成

Proactor:数据读取操作由主线程完成

12.3 模拟 Proactor 模式

1.定义

使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。

2.工作流程
使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下:
(1)主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
(2)主线程调用 epoll_wait 等待 socket 上有数据可读。
(3)当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
(4)睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件。
(5)主线程调用 epoll_wait 等待 socket 可写。
(6)当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。

image-20220102221814688

补充:

recv 函数:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
从 socket 中读取数据
int sockfd : server 的 accecpt socket 
void *buf:buffer 的读起始位置
size_t len:读的结束位置
int flag:一般设置为 0 
返回值:
	0: 对方关闭连接
  -1:发生错误
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值