网络编程笔记(六)-标准IO、epoll、多线程

网络编程笔记(六)-标准IO、epoll、多线程

参考《TCP/IP 网络编程》15、16、17、18 章

套接字和标准 I/O

标准 I/O 函数的 2 个优点:

  • 良好的移植性(Portability)。
  • 利用缓冲提高性能。

使用 read 和 write 函数传输 400M 文件的时间远远大于使用标准函数 fgets 和 fputs。

标准 I/O 函数的几个缺陷:

  • 不容易进行双向通信。如果要同时进行读写操作,则应以 r+、w+、a+ 模式打开。

  • 有时候可能频繁调用 fflush。每次切换读写工作状态都应该调用 fflush 函数。

  • 需要以 FILE 结构体指针形式返回文件描述符;

利用 fdopen 函数将文件描述符转换成 FILE 结构体指针:

#include <stdio.h>  

/*
	fildes: 需要转换的文件描述符  
	mode: 将要创建的 FILE 结构体指针的模式信息 
*/
// 成功时返回转换的FILE结构体指针,失败时返回 NULL  
FILE * fdopen(int fildes, const char *mode);  

利用 fileno 函数将 FILE 结构体指针转换成文件描述符

#include <stdio.h>  

// 成功时返回转换后的文件描述符,失败时返回-1  
int fileno(FILE * stream);  

关于 I/O 分离流的其他内容

I/O 分离流的方式

I/O 流分离的方式:

  • TCP I/O 过程(routine)分离:通过调用 fork 函数复制出一个文件描述符,以区分输入和输出中使用的文件描述符,虽然文件描述符不会根据输入和输出进行区分,但是我们分开了 2 个文件描述符的用途。
  • 调用 2 次调用 fdopen:创建读模式 FILE 指针和写模式 FILE 指针,分离了输入工具和输出工具。

TCP I/O 过程(routine)分离流的好处:

  • 通过分开输入过程(代码)和输出过程降低实现难度。

  • 与输入无关的输出操作可以提高速度。

2 次调用 fdopen 分离流的目的

  • 为了将 FILE 指针按读模式、写模式加以区分。

  • 可以通过区分读写模式降低难度。

  • 通过区分 I/O 缓冲提高缓冲性能。

流分离带来的问题

流分离带来的问题:终止流时无法半关闭。读模式 FILE 指针、写模式 FILE 指针都是基于同一个文件描述符创建的。因此,针对任意一个 FILE 指针调用 fclose 都将关闭文件描述符,终止套接字

image

解决方法:在创建 FILE 指针前复制文件描述符,然后利用各自的文件描述符创建读模式 FILE 指针和写模式 FILE 指针,这样销毁所有文件描述符后才能销毁套接字(引用计数)。

image

image

文件描述符的复制

dup 和 dup2 函数

#include <unistd.h>  
  
int dup(int fildes)  
    
// 成功时返回复制的文件描述符,失败时返回-1
/*
	fildes:需要复制的文件描述符
	filders2:明确指定的文件描述符整数值
*/
int dup2(int fildes, int fildes2)  

无论复制出多少文件描述符,均应调用 shutdown 函数发送 EOF 并进入半关闭状态。

优于 select 的 epoll

select 和 epoll 的比较

select 不适合以 web 服务器端开发为主流的现代开发环境,所以要学习 Linux 平台下的 epoll。

基于 select 的 I/O 复用技术速度慢的原因:

  • 调用 select 函数后常见的针对所有文件描述符的循环语句

  • 每次调用 select 函数时都需要向函数传递监视对象信息。

select 的优点:

  • 服务器端接入者少。

  • 程序具有兼容性——大部分操作系统都支持 select 函数

epoll 函数

epoll_create 函数

epoll_create:向操作系统请求创建保存 epoll 文件描述符的空间,对应 select 方式下声明的 fd_set 变量。该函数返回的文件描述符主要用于区分 epoll 例程。

#include <sys/epoll.h>  

// size: epoll实例的大小
// 返回值:成功时返回epoll文件描述符,失败时返回-1  
int epoll_create(int size)  

注意,size 只是建议 epoll 例程大小,仅供操作系统参考。Linux 2.6.8 之后的内核将完全忽略 size 参数,因为内核会根据情况调整 epoll 例程的大小。

epoll_ctl 函数

epoll_ctl:向空间(对应于 select 中的位数组)注册并注销文件描述符,对应 select 中的 FD_SET、FD_CLR 函数。

#include <sys/epoll.h>

/*
	epfd:	用于注册监视对象的epoll例程的文件描述符。 
	op:		用于指定监视对象的添加、删除或更改。
	fd:		需要注册的监视对象文件描述符。
	event:	监视对象事件类型。
*/
// 成功时返回0,失败时返回-1 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

// 用法:
struct epoll_event event;
event.events = EPOLLIN;		// 详见结构体 epoll_event
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);  

举例:

  • epoll_ctl(A, EPOLL_CTL_ADD, B, C); “epoll 例程中 A 注册文件描述符 B,主要目的是监视参数 C 中的事件。
  • epoll_ctl(A, EPOLL_CTL_DEL, B, NULL); ”从 epoll 例程 A 中删除文件描述符 B。“

op 操作:

  • EPOLL_CTL_ADD:将文件描述符注册到 epoll 例程。
  • EPOLL_CTL_DEL:从 epoll 例程中删除文件描述符。使用时需向第四个参数传递 NULL。
  • EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。

epoll_wait 函数

epoll_wait:等待文件描述符发生变化,类似 select 函数。

#include <sys/epoll.h>
/*
    epfd:		标识事件发生监视范围epoll例程的文件描述符。
    events:		保存发生事件的文件描述符集合的结构体地址值。
    maxevents:	第二个参数中可以保存的最大事件数。 
    timeout:	以毫秒为单位的等待时间,传递-1时,一直等待直到发生事件。
*/
// 返回值:成功时返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。失败时返回-1  
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)  

结构体 epoll_event 和 epoll_data

select 函数中通过 fd_set 变量查看事件发生与否,epoll 通过以下结构体将发生事件的文件描述符集中到一起:

//epoll将发生事件的文件描述符集中在一起,放在epoll_event结构体中  
struct epoll_event{  
    __uint32_t events;  
    epoll_data_t data;  
}

typedef union epoll_data{  
    void * ptr;  
    int fd;  
    __uint32_t u32;  
    __uint64_t u64;  
} epoll_data_t;  

声明足够大的 epoll_event 结构体数组后,传递给 epoll_wait 函数时,发生变化的文件描述符信息将被填入该数组,无需像 select 一样对所有文件描述符进行循环。

epoll_event 成员 epoll_events 中可以保存的常量及所指的事件类型:

image

基于 epoll 的回声服务器端

注意与 select 进行对比。

// echo_epollserv.c

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <unistd.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);

int main(int argc, char *argv[]) {
  int serv_sock, clnt_sock;
  struct sockaddr_in serv_adr, clnt_adr;
  socklen_t adr_sz;
  int str_len, i;
  char buf[BUF_SIZE];
  
  struct epoll_event *ep_events;
  struct epoll_event event;
  int epfd, event_cnt;

  if (argc != 2) {
    printf("Usage : %s <port>\n", argv[0]);
    exit(1);
  }

  serv_sock = socket(PF_INET, SOCK_STREAM, 0);
  if (serv_sock == -1) {
    error_handling("socket error");
  }

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_adr.sin_port = htons(atoi(argv[1]));

  if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
    error_handling("bind() error");
  }

  if (listen(serv_sock, 5) == -1) {
    error_handling("listen() error");
  }

  epfd = epoll_create(EPOLL_SIZE);  //创建epoll例程
  ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
  
  // 将serv_sock添加到例程空间中
  event.events = EPOLLIN; 
  event.data.fd = serv_sock;
  epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);  

  while (1) {
    event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

    if (event_cnt == -1) {
      puts("epoll_wait() error!");
      break;
    }

    for (i = 0; i < event_cnt; i++) {
      if (ep_events[i].data.fd == serv_sock) {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
        
        event.events = EPOLLIN;
        event.data.fd = clnt_sock;
        epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);  //将请求连接的套接字添加到epoll例程中
        printf("connected client : %d \n", clnt_sock);
      } else {  // read message
        str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
        if (str_len == 0) {  // close request!
          epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); // 将断开连接的套接字从epoll例程中移除
          close(ep_events[i].data.fd);
          printf("closed client %d \n", ep_events[i].data.fd);
        } else {
          write(ep_events[i].data.fd, buf, str_len);  // echo!
        }
      }
    }
  }

  close(serv_sock);
  close(epfd);    // 注意关闭epoll例程
  return 0;
}

void error_handling(char *message) {
  fputs(message, stderr);
  fputs("\n", stderr);
  exit(1);
}

边缘触发和条件触发

边缘触发和条件触发的区别——在于发生事件的时间点:

  • 条件触发:只要输入缓冲有数据,就会一直通知该事件。
  • 边缘触发:输入缓冲收到数据时,仅注册一次事件。

epoll 默认以条件触发方式工作,select 也是以条件触发方式工作的。

实现边缘触发服务器的原理:通过 errno 变量验证错误原因;使用 fcntl 函数更改套接字选项,添加非阻塞 O_NONBLOCK 表至,完成非阻塞 I/O。

void setnonblockingmode(int fd){  
	int flag = fcntl(fd, F_GETFL, 0);
	fcntl(fd, F_SETFL, flag|O_NONBLOCK);  
}  

边缘触发的优点:可以分离接收数据和处理数据的时间点。

多线程服务器端的实现

pthread_create 和 pthread_join

  1. pthread_create
    功能:生成线程
    必须在调用此函数前就为 pthread_t 对象分配内存空间(malloc)
    thread_handles = malloc(thread_countsizeof(pthread_t));
    技巧:为每一个线程赋予唯一的 int 型参数 rank
    pthread_create(&thread_handles[thread], NULL, Hello, (void) thread);
    thread_p 是指针!

  2. pthread_join
    分支合并(join)到主线程的直线中
    pthread_join(thread_handles[thread], NULL);
    thread_p 是值!

    类似的函数有 int pthread_detach(pthread_t thread); 调用 pthread_detach 不会引起线程终止或进入阻塞状态。

临界区

  1. 临界区
    实例:计算 pi 的线程函数
    竞争条件
    当多个线程都要访问共享变量或共享文件这样的共享资源时,如果其中一个访问是更新操作,那么这些访问就可能会导致某种错误
    临界区
    一个更新共享资源的代码段,一次只允许一个线程执行该代码段
    必须串行执行临界区的代码

  2. 互斥量(mutex)
    特殊类型 pthread_mutex_t
    初始化和销毁
    pthread_mutex_int
    pthread_mutex_init(&mutex, NULL);
    pthread_mutex_destroy
    pthread_mutex_destroy(&mutex);
    上锁和解锁
    pthread_mutex_lock
    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock
    pthread_mutex_unlock(&mutex);
    使用互斥量时,多个进程进入临界区的顺序是随机的

  3. 信号量(semaphore)
    信号量定义:一种特殊类型的 unsigned int 无符号整型变量,可以赋值为 0、1、2……
    优点
    能够初始化为任何非负值
    信号量没有归属权,任何线程都能够对锁上的信号量进行解锁
    生产者-消费者同步
    一个消费者线程在继续运行前,需要等待一些条件或数据被生产者线程创建
    基本操作
    引入 semaphore 头文件
    和 pthread_t 一样,init 前需要分配内存
    semaphores = malloc(thread_count*sizeof(sem_t));
    初始化和销毁
    sem_init
    sem_destroy
    P 操作和 V 操作
    sem_wait
    将信号量减 1
    如果值变成 负数,则阻塞执行 P 操作的线程,否则线程继续执行
    sem_post
    将信号量加 1
    如果值 小于等于零,则唤醒一个等待进程

多线程并发服务器端的实现(聊天程序)

服务器:可以同时连接多个客户

#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

void *handle_clnt(void *arg);
void send_msg(char *msg, int len);
void error_handling(char *message);

int clnt_cnt = 0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;

int main(int argc, char *argv[]) {
  int serv_sock;
  int clnt_sock;
  struct sockaddr_in serv_adr;
  struct sockaddr_in clnt_adr;
  int clnt_adr_sz;
  pthread_t t_id;       // 传递给线程的id

  if (argc != 2) {
    printf("Usage : %s <port>\n", argv[0]);
    exit(1);
  }

  pthread_mutex_init(
      &mutx, NULL);  // 互斥访问临界区——clnt_cnt和clnt_socks
  serv_sock = socket(PF_INET, SOCK_STREAM, 0);
  if (serv_sock == -1) {
    error_handling("socket() error");
  }

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
  serv_adr.sin_port = htons(atoi(argv[1]));

  if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
    error_handling("bind() error");
  }

  if (listen(serv_sock, 5) == -1) {
    error_handling("listen() error");
  }

  while (1) {
    clnt_adr_sz = sizeof(clnt_adr);
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
    // 以下代码访问临界区,将相关信息写入clnt_cnt和clnt_socks。
    pthread_mutex_lock(&mutx);
    clnt_socks[clnt_cnt++] = clnt_sock;
    pthread_mutex_unlock(&mutx);

    pthread_create(&t_id, NULL, handle_clnt, (void *)&clnt_sock);
    pthread_detach(t_id);   // 从内存中销毁完全已终止的线程
    printf("Connetcted client IP :%s \n", inet_ntoa(clnt_adr.sin_addr));
  }

  close(serv_sock);

  return 0;
}

void *handle_clnt(void *arg) {
  int clnt_sock = *((int *)arg);  // 传递给线程的参数
  int str_len = 0;
  int i;
  char msg[BUF_SIZE];

  while ((str_len = read(clnt_sock, msg, sizeof(msg))) != 0)
    send_msg(msg, str_len);

  pthread_mutex_lock(&mutx);  // 访问临界区前获得锁
  for (i = 0; i < clnt_cnt; i++) {
    if (clnt_sock == clnt_socks[i]) { // 覆盖删除
      while (i++ < clnt_cnt - 1) clnt_socks[i] = clnt_socks[i + 1];
      break;
    }
  }

  clnt_cnt--;
  pthread_mutex_unlock(&mutx);
  close(clnt_sock);

  return NULL;
}

// send to all
void send_msg(char *msg, int len) {
  int i;

  pthread_mutex_lock(&mutx);
  for (i = 0; i < clnt_cnt; i++) {
    write(clnt_socks[i], msg, len);
  }

  pthread_mutex_unlock(&mutx);
}

void error_handling(char *message) {
  fputs(message, stderr);
  fputs("\n", stderr);
  exit(1);
}

客户端:分离输入和输出而创建线程

#include <arpa/inet.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define BUF_SIZE 100
#define NAME_SIZE 20

void *send_msg(void *arg);
void *recv_msg(void *arg);
void error_handling(char *message);

char name[NAME_SIZE] = "[DEFAULT]";
char msg[BUF_SIZE];

int main(int argc, char *argv[]) {
  int sock;
  struct sockaddr_in serv_adr;
  pthread_t snd_thread, rcv_thread;
  void *thread_return;

  if (argc != 4) {
    printf("Usage : %s <IP> <port> <name>\n", argv[0]);
    exit(1);
  }

  sprintf(name, "[%s]", argv[3]);
  sock = socket(PF_INET, SOCK_STREAM, 0);
  if (sock == -1) {
    error_handling("socket() error");
  }

  memset(&serv_adr, 0, sizeof(serv_adr));
  serv_adr.sin_family = AF_INET;
  serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
  serv_adr.sin_port = htons(atoi(argv[2]));

  if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
    error_handling("connect() error\r\n");
  } else {
    printf("connect to the server!\n");
  }

  pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);
  pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
  pthread_join(snd_thread, &thread_return);
  pthread_join(rcv_thread, &thread_return);

  close(sock);
  return 0;
}

void error_handling(char *message) {
  fputs(message, stderr);
  fputs("\n", stderr);
  exit(1);
}

void *send_msg(void *arg) {
  int sock = *((int *)arg);
  char name_msg[NAME_SIZE + BUF_SIZE];

  while (1) {
    fgets(msg, BUF_SIZE, stdin);
    if (!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")) {
      close(sock);
      exit(0);
    }
    sprintf(name_msg, "%s %s", name, msg);
    write(sock, name_msg, strlen(name_msg));
  }

  return NULL;
}

void *recv_msg(void *arg) {
  int sock = *((int *)arg);
  char name_msg[NAME_SIZE + BUF_SIZE];
  int str_len;

  while (1) {
    str_len = read(sock, name_msg, NAME_SIZE + BUF_SIZE - 1);
    if (str_len == -1) {
      return (void *)-1;
    }
    name_msg[str_len] = 0;
    fputs(name_msg, stdout);
  }

  return NULL;
}

参考资料

https://github.com/chankeh/net-lenrning-reference

posted @ 2021-10-24 16:36  CoolGin  阅读(154)  评论(0编辑  收藏  举报