Linux网络编程-socket编程

1. 套接字(socket)概念

在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,我们可以用文件描述符引用套接字。在TCP/IP 协议中,“IP地址 + TCP 或 UDP 端口号”唯一标识网络通信中的一个进程。“IP地址 + 端口号” 就对应一个socket。想要建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair 就唯一标识一个连接。因此可以用socket来描述网络连接的一对一关系。
套接字通信原理:
在这里插入图片描述

在网络通信中,套接字一定是成对出现的。

2.预备知识

2.1 网络字节序

小端法:高位存在高地址,低位存在低地址
大端法:高位存在低地址,低位存在高地址

在这里插入图片描述

TCP/IP协规定,网络数据流应采用大端字节序,即低位高地址,高位低地址
伪使网络程序具有可移植性,使同样的c代码在大端和小端计算机上编译后都能正常运行,可以调用一下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //h - to - n - l 表示本机字节序转网络字节序(IP协议)
uint16_t htons(uint16_t hostshort); //本地 到 网络(port)
uint32_t ntohl(uint32_t netlong); //网络 到 本地(IP)
uint16_t ntohs(uint16_t netshort);//网络 到 本地(port)
/*
h表示host
n表示network
l表示32位长整数
s表示16位短整数
*/

2.2 IP地址转换函数(string – int)

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst); //将点分十进制的IP转换位网络IP
参数:
	af:指定IP协议类型, ipv4、ipv6
		AF_INET:ipv4
		AF_INET6:ipv6
	src:传入参数,ip地址(点分十进制)
	dst:传出参数,转换后的ip地址
返回值:
	成功:1
	异常:0,说明src指向的不是一个有效的IP地址
	失败:-1

const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);//将网络IP 转换为 本地的IP地址
参数:
	af:指定IP协议类型, ipv4、ipv6
		AF_INET:ipv4
		AF_INET6:ipv6
	src:传入参数,网络ip地址
	dst:传出参数,转换后的ip地址(点分十进制)
	size:dst缓冲区的大小
返回值:
	成功:返回dst
	失败:NULL

2.3 sockaddr 数据结构

struct sockaddr 恨锁网络编程函数诞生早于 IPV4,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void*)的作用,传递一个地址给函数,至于这个函数是sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转换为所需要的地址类型。(可以参看 man 7 ip
在这里插入图片描述

struct sockaddr_in {
	sa_family_t sin_family;     /* 地址族: AF_INET */
	u_int16_t sin_port;         /* 按网络字节次序的端口*/
	struct in_addr sin_addr;    /* internet地址 */
};

/* Internet地址. */
struct in_addr {
	u_int32_t s_addr;           /* 按网络字节次序的地址 */
};

3.网络套接字函数

3.1 socket模型创建流程

在这里插入图片描述

3.2 socket 函数

函数作用:创建一个socket套接字

函数原型:
	#include <sys/types.h>          /* See NOTES */
	#include <sys/socket.h>
	int socket(int domain, int type, int protocol);
函数参数:
	domain:指定IP地址协议(AF_INET、AF_INET6、AF_UNIX)
	type:以何种形式传输
		SOCK_STREAM:流式
		SOXCK_DGRAM:报式
	protocol:默认传0
返回值:
	成功:返回新套接字对应的文件描述符
	失败:-1, 设置errno

3.3 bind 函数

函数作用:将 IP + port(地址结构) 与 socket 绑定。

函数原型:
	int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
函数参数:
	sockfd:socket对应的文件描述符
	my_addr:IP + port 对应的结构体变量(地址结构)
	addrlen:参数my_addr的大小
返回值:
	成功:0
	失败:-1, 设置errno

3.4 listen 函数

函数作用:设置同时与服务器建立连接的上限数

函数原型:
	int listen(int sockfd, int backlog);
函数参数:
	sockfd:socket文件描述符
	backlog:上限数(最大设置128)
函数返回值:
	成功:0
	失败:-1 ,设置errno

3.5 accept 函数

函数作用:阻塞等待客户端建立连接,成功,返回一个与客户端成功连接的socket文件描述符

函数原型:
	int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数参数:
	sockfd:socket文件描述符
	addr:传出参数,成功建立连接的那个客户端的地址结构(IP + port)
	addrlen:传入传出参数,传入addr的大小,传出客户端addr的实际大小
返回值:
	成功:返回一个非负整数(能与服务器进行通信的socket对应的文件描述符cfd)
	失败:-1,设置errno

3.6 connect 函数

函数作用:使用现有的socket文件描述符与服务器建立连接

函数原型:
	int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数参数:
	sockfd:socket文件描述符
	addr:传入参数,需要与服务器建立连接的服务器地址结构
	addrlen:addr大小
返回值:
	成功:0
	失败:-1 errno

3.7 编写C/S模型示例

客户端向服务器发送 hello world! 服务器转大写后回给客户端,客户端打印。

//服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <fcntl.h>

void sys_err(const char* str) {
	perror(str);
	exit(1);
}

int main() {

	int sev_fd, c_fd;
	int ret;
	//创建socket
	sev_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(sev_fd < 0) {
		sys_err("socket error\n");
	}

	//绑定
	struct sockaddr_in sev_addr;
	memset(&sev_addr, 0, sizeof(sev_addr));
	sev_addr.sin_family = AF_INET;
	sev_addr.sin_port = htons(8888);
	sev_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(sev_fd, (struct sockaddr*)&sev_addr, sizeof(sev_addr));
	if(ret < 0) {
		sys_err("bind error\n");
	}

	//设置监听
	ret = listen(sev_fd, 128);
	if(ret < 0) {
		sys_err("listen error\n");
	}


	//阻塞等待客户端的连接
	struct sockaddr_in cli_addr;
	socklen_t len = sizeof(cli_addr);
	c_fd = accept(sev_fd, (struct sockaddr*)&cli_addr, &len);
	if(ret < 0) {
		sys_err("accept error\n");
	}

	char buf[1024];
	int n;
	
	//读信息
	n = read(c_fd, buf, sizeof(buf));
	if(n < 0) {
		sys_err("read error\n");
	}

	ret = write(STDOUT_FILENO, buf, n);
	if(ret < 0) {
		sys_err("write error\n");
	}
	
	int i = 0;
	for(i = 0; i < n; i++) {
		if(buf[i] >= 'a' && buf[i] <= 'z') {
			buf[i] -= 32;
		}
	}
	
	//写信息
	ret = write(c_fd, buf, n);
	if(ret < 0) {
		sys_err("write error\n");
	}
	
	sleep(1);
	close(sev_fd);
	close(c_fd);
	return 0;
}
//客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <fcntl.h>

void sys_err(const char* str) {
	perror(str);
	exit(1);
}

int main() {

	int cli_fd;
	int ret;

	//创建socket
	cli_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(cli_fd < 0) {
		sys_err("socket error\n");
	}

	//连接服务器
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = inet_addr("127.0.0.1");
	ret = connect(cli_fd, (struct sockaddr*)&serv, sizeof(serv));
	if(ret < 0) {
		sys_err("connect error\n");
	}

	char* str = "hello world!\n";
	char buf[1024];
	int n;
	memset(buf, 0, sizeof(buf));

	//写入信息
	ret = write(cli_fd, str, strlen(str));
	if(ret < 0) {
		sys_err("write errorn\n");
	}

	n = read(cli_fd, buf, sizeof(buf));
	if(n < 0) {
		sys_err("read error\n");
	}
	//printf("%s\n", buf);
	write(STDOUT_FILENO, buf, n);	

	close(cli_fd);
	return 0;
}

4.高并发服务器

4.1 查错代码的封装

wrap.h

#ifndef _WRAP_H_
#define _WRAP_H_

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

void sys_err(const char* str);
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int Socket(int domain, int type, int protocol);
int Bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
int Listen(int sockfd, int backlog);
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


#endif

wrap.c

#include "wrap.h"

//错误处理函数
void sys_err(const char* str) {
	perror(str);
	exit(1);
}

//socket创建套接字
int Socket(int domain, int type, int protocol) {
	int fd;
	fd = socket(domain, type, protocol);
	if(fd < 0) {
		sys_err("socket error\n");
	}

	return fd;
}

//bind绑定
int Bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen) {
	int ret;

	ret = bind(sockfd, my_addr, addrlen);
	if(ret < 0) {
		sys_err("bind error\n");
	}

	return ret;
}

//listen设置监听
int Listen(int sockfd, int backlog) {
	int ret;

	ret = listen(sockfd, backlog);
	if(ret < 0) {
		perror("listen error\n");
	}

	return ret;
}

//accept等待客户端连接
int Accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
	int fd;

	fd = accept(sockfd, addr, addrlen);
	if(fd < 0) {
		perror("accept error\n");
	}

	return fd;
}

//cannect连接服务器
int Connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
	int ret;

	ret = connect(sockfd, addr, addrlen);
	if(ret < 0) {
		perror("connect error\n");
	}

	return ret;
}

read函数的返回值:

  1. 大于0:实际读取到的字节数
  2. 等于0:已经读到末尾(对端以及关闭)
  3. -1:应进一步判断errno的值

4.2 多进程并发服务器

使用多进程并发服务器是要考虑一下几点:

  1. 父进程最大文件描述符个数(父进程中需要close关闭accept产生的新的文件描述符)
  2. 系统内创建进程个数(与内存大小无关)
  3. 进程创建过多是否降低整体服务性能(进程调度)

示例·:Server

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <fcntl.h>

#include "./Inc/wrap.h"

void sig_wait(int signum) {
    
    while(waitpid(0, NULL, WNOHANG) > 0);
    return;
}

int main() {

    int lfd, cfd;
    pid_t pid;
    int n;
    char buf[1024];
    struct sigaction act;
    act.sa_handler = sig_wait;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, NULL);
    

    lfd = Socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv, cli;
    socklen_t len;
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9999);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    Listen(lfd, 128);

    while(1) {
        len = sizeof(cli);
        cfd = Accept(lfd, (struct sockaddr*)&cli, &len);
        pid = fork();
        if(pid < 0) {
            sys_err("fork error");
        }
        else if(pid > 0) {
            close(cfd);
        }
        else if(pid == 0) {
            close(lfd);

            while((n = read(cfd, buf, 1024)) != 0) {
                int i;
                for(i = 0; i < n; i++) {
                    buf[i] = toupper(buf[i]);
                }

                write(STDOUT_FILENO, buf, n);
                write(cfd, buf, n);
            }
            return 0;
        }
    }
    close(lfd);
    return 0;
}

4.3 多线程并发服务器

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>

#include "Inc/wrap.h"

#define SER_PORT 9999

struct sin_info {
    struct sockaddr_in cli_addr;
    int cfd;
};

void* cli_back(void* arg) {
    char buf[1024];
    char ip_buf[16];
    int n, i;
    struct sin_info s = *(struct sin_info*)arg;

    printf("[ip:%s, port:%d]连接成功!!!\n", inet_ntop(AF_INET, &s.cli_addr.sin_addr, ip_buf, sizeof(ip_buf)), ntohs(s.cli_addr.sin_port));

    while(1) {
        n = read(s.cfd, buf, sizeof(buf));
        if(n == 0) break;

        printf("[ip:%s, port:%d]:\n", inet_ntop(AF_INET, &s.cli_addr.sin_addr, ip_buf, sizeof(ip_buf)), ntohs(s.cli_addr.sin_port));
        write(STDOUT_FILENO, buf, n);
        for(i = 0; i < n; i++) {
            buf[i] = toupper(buf[i]);
        }
        write(s.cfd, buf, n);
    }
	close(s.cfd);
    return NULL;
}

int main() {
    int lfd, cfd;
    int len, ret;
    int i = 0;
    struct sin_info s[128];
    pthread_t tid;
    //创建socket
    lfd = Socket(AF_INET, SOCK_STREAM, 0);
    //绑定
    struct sockaddr_in serv, cli_addr;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(SER_PORT);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));
    //设置监听
    Listen(lfd, 128);
    //监听,等待客户端连接请求
    while(1) {
        len = sizeof(cli_addr);
        //阻塞监听,等待客户端连接
        cfd = Accept(lfd, (struct sockaddr*)&cli_addr, &len);
        s[i].cli_addr = cli_addr;
        s[i].cfd = cfd;

        //创建线程,处理连接到的客户端,用于与客户端通信
        ret = pthread_create(&tid, NULL, cli_back, (void*)&s[i]);
        if(ret != 0) {
            fprintf(stderr, "pthread_create error:%s", strerror(ret));
            exit(1);
        }
        //设置线程分离,防止出现僵尸线程
        ret = pthread_detach(tid);
        if(ret != 0) {
            fprintf(stderr, "pthread_detach error:%s", strerror(ret));
            exit(1);
        }
    }
    close(lfd);
    return 0;
}

4.4 多路I/O转接服务器

多路I/O转接都武器也叫多任务I/O服务器。该类服务器实现的主旨思想是,不再由应用程序自己监听客户端连接请求,取而代之由内核替应用程序监听文件。
主要方法有三种:select、poll、epoll

4.4.1 select

  1. select能监听的文件描述符个数受限于FD_SETSIZE, 一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数。
  2. 解决1024以下客户端时使用select很合适的,如果连接客户端过多,select采用的是轮询模型,会大幅度降低服务器响应效率,不应在select上投入更多精力。

select函数

	#include <sys/select.h>

	/* According to earlier standards */
	#include <sys/time.h>
	#include <sys/types.h>
	#include <unistd.h>
函数原型:
	int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函数参数:
	nfds:监听的文件描述符里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
	readfds:监控有读数据到达文件描述符集合,传入传出参数
	writefds:监控写数据到达文件描述符集合,传入传出参数
	exceptfds:监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
	timeout:定时阻塞监听时间
		NULL:永远等下去
		设置timeout,等待固定时间
		设置timeout里时间均为0,检查描述字后立即返回,轮询
返回值:
	大于0:所有监听集合中满足对应事件的总数
	0:没有满足监听条件的文件描述符
	-1:设置errno
struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* microseconds */
};
void FD_CLR(int fd, fd_set *set);//将fd从文件描述符集合中清除
int  FD_ISSET(int fd, fd_set *set);//判断fd是否在文件描述符集合中
void FD_SET(int fd, fd_set *set);//将fd加入文件描述符集合中
void FD_ZERO(fd_set *set);//将文件描述符集合置0

示例:select模型服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <ctype.h>

#include "./Inc/wrap.h"

#define SER_PORT 9999

int main() {

    int lfd, cfd;
    struct sockaddr_in serv, cli;
    socklen_t len;
    fd_set rset, allset;
    int maxfd;
    int ret, i, j;
    int n;
    char buf[1024];

    //创建socket
    lfd = Socket(AF_INET, SOCK_STREAM, 0);

    //绑定
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(SER_PORT);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    //设置监听
    Listen(lfd, 128);

    //select处理读取事件
    //初始化文件描述符集合
    FD_ZERO(&allset); //将文件描述符集合清0
    FD_SET(lfd, &allset); //将监听文件描述符加入集合,监听连接请求的读事件
    maxfd = lfd;

    while(1) {
        rset = allset;
        ret = select(maxfd + 1, &rset, NULL, NULL, NULL);
        if(ret < 0) {
            sys_err("select error");
        }

        //判断是否有lfd相关的事件到达
        if(FD_ISSET(lfd, &rset)) {
            len = sizeof(cli);
            cfd = Accept(lfd, (struct sockaddr*)&cli, &len);

            //将用于与客户端通信的新文件描述符加入集合
            FD_SET(cfd, &allset);//监听数据的读事件

            //判断最大文件描述符是否发生变化
            if(maxfd < cfd) {
                maxfd = cfd;
            }

            //如果--ret为0,则说明只有一个事件到达,且该事件为监听事件
            if(--ret == 0) {
                continue;
            }
        }

        //循环处理通讯事件
        for(i = lfd + 1; i <= maxfd; i++) {
            if(FD_ISSET(i, &rset)) {
                if((n = read(i, buf, sizeof(buf))) == 0) {
                    FD_CLR(i, &allset);
                    close(i);
                    continue;
                }
                if(n < 0) {
                    sys_err("read error");
                }

                write(STDOUT_FILENO, buf, n);

                for(j = 0; j < n; j++) {
                    buf[j] = toupper(buf[j]);
                }

                write(i, buf, n);

            }
        }

    }
    FD_CLR(lfd, &allset);
    close(lfd);
    
    return 0;
}

select的优缺点:
缺点:1.监听上限受文件描述符限制,最大1024;2.检测满足条件的fd时,只能通过程序员添加业务逻辑提高效率,增加了开发难度。
优点:跨平台完成文件描述符监听。

4.4.2 poll

函数原型:
	#include <poll.h>
	int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数:
	fds:监听的文件描述符 数组
	nfds:监听数组的实际有效监听个数
	timeout:超时时长。单位:毫秒
		-1:阻塞等待
		0:立即返回
		>0:等待指定毫秒数,如系统时间精度不准确,则向上取值
返回值:返回满足对应监听事件的文件描述符个数
	

struct pollfd {
	int   fd;         /* file descriptor (待监听的文件描述符)*/
	short events;     /* requested events (待监听的文件描述符对应的监听事件:POLLIN-读、POLLOUT-写、POLLERR-异常事件)*/
 	short revents;    /* returned events (传入 0, 如果对应事件满足,返回 非0,即:POLLIN-读、POLLOUT-写、POLLERR-异常事件)*/
};

示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <ctype.h>

#include "./Inc/wrap.h"

#define SER_PORT 9999
#define CLI_MAX 1024
#define MAXLINE 1024

int main() {

    int lfd, cfd;
    struct sockaddr_in serv, cli;
    socklen_t len;
    struct pollfd fds[CLI_MAX];
    int maxi, nready, i, n, j;
    char buf[MAXLINE], ip_buf[INET_ADDRSTRLEN];
    //1 创建socket
    lfd = Socket(AF_INET, SOCK_STREAM, 0);

    //2 绑定
    //2.1 设置端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    //2.2 清空serv结构体
    memset(&serv, 0, sizeof(serv));

    //2.3 初始化 serv
    serv.sin_family = AF_INET;
    serv.sin_port = htons(SER_PORT);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);

    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    //3 设置监听
    Listen(lfd, 128);

    //4 poll监听读事件
    //4.1 向监听文件描述符数组中加入 lfd
    fds[0].fd = lfd;
    fds[0].events = POLLIN;

    //4.2 将未占用的位置的fd全部置为-1,作为判断该位置没有被占用的标志
    for(i = 1; i < CLI_MAX; i++) {
        fds[i].fd = -1;
    }

    maxi = 0; //设置监听文件描述符数组中实际监听的最大下标

    //4.2 循环监听读事件
    while(1) {
        nready = poll(fds, maxi + 1, -1);
        if(nready < 0) {
            sys_err("poll error");
        }

        if(fds[0].revents & POLLIN) {
            len = sizeof(cli);
            cfd = Accept(fds[0].fd, (struct sockaddr*)&cli, &len);

            printf("[ip:%s, port:%d]已连接!!!\n", 
                inet_ntop(AF_INET, &cli.sin_addr, ip_buf, sizeof(ip_buf)), 
                ntohs(cli.sin_port));

            //寻找空闲的位置,将新产生的cfd添加进监听数组
            for(i = 1; i < CLI_MAX; i++) {
                if(fds[i].fd < 0) {
                    fds[i].fd = cfd;
                    fds[i].events = POLLIN;
                    break;
                }
            }

            //如果没有找到空闲的位置,说明达到了最大客户端连接
            if(i == CLI_MAX) {
                sys_err("too many clients");
            }

            //判断添加后,最大的文件描述符下标是否发生变化
            if(maxi < i) {
                maxi = i;
            }

            //如果nready为1,说明此时只有lfd事件到达
            if(--nready <= 0) {
                continue;
            }
        }

        for(i = 1; i <= maxi; i++) {
            //如果该位置为空,则跳过这一次循环
            if(fds[i].fd < 0) {
                continue;
            }

            //如果对应i位置有读事件发生,则收发数据
            if(fds[i].revents & POLLIN) {
                n = read(fds[i].fd, buf, sizeof(buf));
                if(n < 0) {
                    sys_err("read error");
                } 

                if(n == 0) {
                    close(fds[i].fd);
                    fds[i].fd = -1;
                }

                write(STDOUT_FILENO, buf, n);

                for(j = 0; j < n; j++) {
                    buf[j] = toupper(buf[j]);
                }

                write(fds[i].fd, buf, n);
            }
        }
    }

    close(fds[0].fd);
    fds[0].fd = -1;
    return 0;
}

poll的优缺点:
优点:1.自带数组结构;2.可以将监听事件集合,和返回事件集合分离;3.可以拓展监听上限,可超出1024限制
缺点:1、不能跨平台(使用局限性);2.无法直接定位满足监听事件的文件描述符,编码难度较大

4.4.3 epoll

epoll 是 Linux 下多路复用IO接口 select/poll 的增强版,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为他会复用文件描述符集合来传递结果,而不用迫使开发者每次等待事件之前都必须重新准备被监听的文件描述符集合,另一点原因就是获取事件的时候,它无需遍历整个文件描述符集合,只遍历那些被内核 IO 事件异步唤醒而加入Ready队列的描述符集合。
目前 epoll 是 Linux 大规模并发网络程序中的热门首选模型。
epoll除了提供 select/poll 那种IO事件的电平触发外,还提供了边沿触发,这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait 的调用,提高应用程序效率。
可以使用 cat 命令查看一个进程可以打开的socket文件描述符上限。

cat /proc/sys/fs/file-max

如果有需要,可以通过修改配置文件的方式修改该上限值。

sudo vim /etc/security/limits.conf
#在文件尾部写入一下配置,soft软限制,hard硬限制。
* soft nofile 65535
* hard nofile 100000
4.4.3.1 epoll_create 函数

函数作用:创建一颗监听红黑树,打开一个epoll文件描述符(内核创建一颗红黑树,返回的文件描述符指向树根)

#include <sys/epoll.h>
函数原型:
	int epoll_create(int size);
函数参数:
	size:创建的红黑树的监听节点的数量。(仅供内核参考)
返回值:
	成功:返回文件描述符,指向新创建的红黑树的根节点
	失败:-1, 设置errno
4.4.3.2 epoll_ctl 函数

函数作用:操作一颗监听红黑数

#include <sys/epoll.h>
函数原型:
	int epoll_ctl(int epfd, int  op,  int  fd, struct epoll_event *event);
函数参数:
	epfd:epoll_create返回的文件描述符
	op:对该监听红黑树所做的操作
		EPOLL_CTL_ADD:添加一个节点到监听红黑树
		EPOLL_CTL_MOD:修改监听红黑树上的监听事件
		EPOLL_CTL_DEL:从监听红黑树删除一个节点
	fd:所要操作的fd
	event:本质是struct epoll_event类型的结构体地址
		events:事件
			EPOLLIN:读事件
			EPOLLOUT:写事件
			EPOLLERR:异常事件
		data:联合体
			int fd; //对应监听事件的fd
		
返回值:
	成功:0
	失败:-1,设置errno

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 */
};
4.4.3.3 epoll_wait 函数

函数作用:监听一颗监听红黑树

#include <sys/epoll.h>
函数原型:
	int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数参数:
	epfd:epoll_create返回的文件描述符
	events:传出参数,用来存内核得到事件的集合(满足监听条件的fd 结构体数组),可以简单看作一个数组。
	maxevents:数组的大小
	timeout:超时时长。单位:毫秒
		-1:阻塞等待
		0:立即返回
		>0:等待指定毫秒数,如系统时间精度不准确,则向上取值
返回值:
	>0:满足监听条件的总个数,可用作循环上限
	=0:没有满足监听条件的事件
	-1:失败,设置errno

示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <ctype.h>

#include "./Inc/wrap.h"

#define SER_PORT 9999
#define CLI_MAX 1024
#define MAXLINE 1024

int main() {

    int lfd, cfd;
    struct sockaddr_in serv, cli;
    socklen_t len;
    int epfd;
    int ret, nready, i, j, n;
    char buf[MAXLINE];
    struct epoll_event ev, evs[CLI_MAX + 1];

    //1.创建socket
    lfd = Socket(AF_INET, SOCK_STREAM, 0);

    //2.设置端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    //初始化serv
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(SER_PORT);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);

    //绑定
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    //设置监听
    Listen(lfd, 128);

    //创建epoll红黑树
    epfd = epoll_create(CLI_MAX);
    if(epfd < 0) {
        sys_err("epoll_create error");
    }

    //初始化ev临时变量
    ev.events = EPOLLIN;
    ev.data.fd = lfd;

    //将监听文件描 lfd 述符上树
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if(ret < 0) {
        sys_err("epoll_ctl error");
    }

    //循环监听epoll红黑树的读事件
    while(1) {
        nready = epoll_wait(epfd, evs, CLI_MAX + 1, -1);
        if(nready < 0) {
            sys_err("epoll_wait error");
        }

        //nready >= 0,循环遍历evs中存入的fd,用于通信或建立连接
        for(i = 0; i < nready; i++) {
            if(evs[i].data.fd == lfd) { //如果lfd有读事件产生,就调用accept建立连接
                len = sizeof(cli);
                cfd = Accept(lfd, (struct sockaddr*)&cli, &len);

                //将产生的新cfd存入临时变量ev
                ev.events = EPOLLIN;
                ev.data.fd = cfd;
                //将产生新连接的cfd加入监听红黑树
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                if(ret < 0) {
                    sys_err("epoll_ctl error");
                }

            }else { // 当此fd 不是lfd ,就是cfd,应用于通信

                cfd = evs[i].data.fd;
                //收发数据
                n = read(cfd, buf, sizeof(buf));
                if(n < 0) {
                    perror("read error");
                    //将此文件描述符对应的节点下数
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
                    if(ret < 0) {
                        sys_err("epoll_ctl_del error");
                    }

                    close(cfd);
                }
                else if(n == 0) { //连接关闭

                    //将此文件描述符对应的节点下数
                    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
                    if(ret < 0) {
                        sys_err("epoll_ctl_del error");
                    }

                    close(cfd);
                }

                write(STDOUT_FILENO, buf, n);

                for(j = 0; j < n; j++) {
                    buf[j] = toupper(buf[j]);
                }

                write(cfd, buf, n);
            }
        }
    }

    close(lfd);
    ret = epoll_ctl(epfd, EPOLL_CTL_DEL, lfd, NULL);
    if(ret < 0) {
        sys_err("epoll_ctl error");
    }
    return 0;
}

4.5 epoll 进阶

4.5.1 事件模型

EPOLL事件有两种模型

  • ET:边缘触发只有数据到来才能触发,不管缓冲区是否还有数据(缓冲区未读尽的数据不会导致epoll_wait返回,只有新事件到来,才能触发返回)
  • LT:水平触发只要有数据都会触发(缓冲区未读尽的数据会导致epoll_wait返回)

测试LT与ET:
ET-Server

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

#include "./Inc/wrap.h"

int main() {
    int lfd, cfd;
    struct sockaddr_in serv, cli;
    socklen_t len;
    int n;
    char buf[10];
    int epfd;
    struct epoll_event ev, evs[10];
    int nready;

    lfd = Socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9999);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    Listen(lfd, 128);

    len = sizeof(cli);
    cfd = Accept(lfd, (struct sockaddr*)&cli, &len);

    epfd = epoll_create(10);

    ev.events = EPOLLIN | EPOLLET;
    //ev.events = EPOLLIN;
    ev.data.fd = cfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

    while(1) {
        nready = epoll_wait(epfd, evs, 10, -1);

        printf("nready = %d\n", nready);
        if(evs[0].data.fd == cfd) {
            n = read(cfd, buf, 5);

            write(STDOUT_FILENO, buf, n);
        }
    }
    close(lfd);

    return 0;
}

LT-Server

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

#include "./Inc/wrap.h"

int main() {
    int lfd, cfd;
    struct sockaddr_in serv, cli;
    socklen_t len;
    int n;
    char buf[10];
    int epfd;
    struct epoll_event ev, evs[10];
    int nready;

    lfd = Socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(9999);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    Bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

    Listen(lfd, 128);

    len = sizeof(cli);
    cfd = Accept(lfd, (struct sockaddr*)&cli, &len);

    epfd = epoll_create(10);

    //ev.events = EPOLLIN | EPOLLET;
    ev.events = EPOLLIN;
    ev.data.fd = cfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);

    while(1) {
        nready = epoll_wait(epfd, evs, 10, -1);

        printf("nready = %d\n", nready);
        if(evs[0].data.fd == cfd) {
            n = read(cfd, buf, 5);

            write(STDOUT_FILENO, buf, n);
        }
    }
    close(lfd);

    return 0;
}

Client客户端

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

#include "./Inc/wrap.h"

int main() {

    int cfd;
    int i;
    char buf[10];
    char ch = 'a';

    cfd = Socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in serv;
    serv.sin_family = AF_INET;
	serv.sin_port = htons(9999);
	serv.sin_addr.s_addr = inet_addr("127.0.0.1");
	connect(cfd, (struct sockaddr*)&serv, sizeof(serv));

    while(1) {
        for(i = 0; i < 5; i++) {
            buf[i] = ch;
        }
        buf[i - 1] = '\n';
        ch++;

        for(; i < 10; i++) {
            buf[i] = ch;
        }
        buf[i - 1] = '\n';
        ch++;

        write(cfd, buf, sizeof(buf));
        sleep(5);
    }
    close(cfd);
    return 0;
}

比较:
LT:LT是缺省的工作模式,并且同时支持 block 和 noblock 。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对一个就绪的文件描述符进行 IO 操作。如果你不做任何操作,内核会继续通知你,所以这种模式的出错率较小,传统的 select/poll 都是这种模式。
ET:ET是高速工作模式,只支持noblock(非阻塞),在这种模式下,当文件描述符从未就绪变为就绪,内核会通过epoll通知你,然后它会假设你知道这个文件描述符已经就绪,并且不会再为那个文件描述符发送更多就绪通知,请注意,如果一直不对这个fd做IO操作(从而导致它无法再次变为就绪),内核不会发送更多的通知。可加入一下代码,使cfd(用于通信的socket文件描述符)非阻塞:

flag = fcntl(cfd, F_GETFI); //获取cfd文件描述符的属性
flag |= O_NONBLOCK; //将获取到的flag 与 O_NONBLOCK 位或 运算,使flag获得非阻塞属性
ret = fcntl(cfd, F_SETFI, flag);//将xfd属性设置为 flag

4.5.2epoll优缺点

优点:高效.
缺点:不能跨平台,只能Linux

4.5.3 epoll反应堆模型

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

#define MAX_EVENTS 1024 //监听上限数
#define BUFLINE 4096 //缓冲区上限
#define SERV_PORT 8080 //端口号

void recvdata(int fd, int events, void* arg); //接收数据的回调函数
void senddata(int fd, int events, void* arg); //发送数据的回调函数

/*
描述就绪文件描述符相关信息
    fd          文件描述符
    events      对应的监听事件
    arg         泛型参数
    call_back   回调函数
    status      是否在监听的标志位:1-表示在监听;2-表示不在监听
    buf         缓冲区
    len         缓冲区大小
    last_active 记录每次加入红黑树g_efd的事件只
*/
struct myevent_s {
    int fd;
    int events;
    void* arg;
    void (*call_back)(int fd, int events, void *arg);
    int status;
    char buf[BUFLINE];
    int len;
    long last_active;
};

int g_efd; //全局变量,保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS + 1]; //自定义结构体数组,+1因为还有一个lfd(监听文件描述符)

//初始化结构体 myevent_s 成员变量
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg) {
    ev->fd = fd;
    ev->call_back = call_back;
    ev->events = 0;
    ev->arg = arg;
    ev->status = 0;
    if(ev->len <= 0) {
        memset(ev->buf, 0, sizeof(ev->buf));
        ev->len = 0;
    }
    
    ev->last_active = time(NULL);

    return;
}

//向epoll监听的红黑树添加一个 文件描述符
void eventadd(int efd, int events, struct myevent_s *ev) {
    struct epoll_event epv; //定义可以添加到监听红黑树的变量
    memset(&epv, 0, sizeof(epv)); //将该变量清0
    int op;
    epv.data.ptr = ev;
    epv.events = ev->events = events; //EPOLLIN 或EPOLLOUT

    if(ev->status == 0)  {   //说明不再 g_efd 监听红黑树上
        op = EPOLL_CTL_ADD;
        ev->status = 1;
    }

    if(epoll_ctl(efd, op, ev->fd, &epv) < 0) {
        perror("epoll_ctl add error");
    }
    else {
        printf("event add ok [fd = %d, events = %0X]\n", ev->fd, ev->events);
    }

    return;
}

//从监听红黑树上删除节点
void eventdel(int efd, struct myevent_s *ev) {
    struct epoll_event epv; //定义可以添加到监听红黑树的变量
    memset(&epv, 0, sizeof(epv)); //将该变量清0

    if(ev->status != 1) { //ev->status 不再树上
        return;
    }
    epv.data.ptr = NULL;
    ev->status = 0;

    //epoll_ctl(efd, op, ev->fd, &epv);

    if(epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv) < 0) {
        perror("epoll_ctl add error");
    }
    else {
        printf("event del ok [fd = %d, events = %0X]\n", ev->fd, ev->events);
    }

    return;
}

//连接客户端回调函数
void acceptcon(int fd, int events, void* arg) {

    struct sockaddr_in cin;
    socklen_t len = sizeof(cin);
    int i;
    int cfd = accept(fd, (struct sockaddr*)&cin, &len);
    if(cfd < 0) {
        if(errno != EAGAIN && errno != EINTR) {
            sleep(1);
        }
        printf("%s:accept, %s\n", __func__, strerror(errno));
        return;
    }

    do
    {
        for(i = 0; i < MAX_EVENTS; i++) {
            if(g_events[i].status == 0) {
                break;
            }
        }
        if(i == MAX_EVENTS) {
            printf("%s:max connect limit[%s]\n", __func__, strerror(errno));
            break;
        }
        int flag = fcntl(cfd, F_GETFL);
        if(flag < 0) {
            printf("%s:fcntl nonblock failed, %s\n", __func__, strerror(errno));
            break;
        }
        flag |= O_NONBLOCK;
        fcntl(cfd, F_SETFL, flag);

        eventset(&g_events[i], cfd, recvdata, &g_events[i]);

        eventadd(g_efd, EPOLLIN, &g_events[i]);
    } while (0);
    
    printf("new client [%s: %d], [time:%ld], pos[%d]\n", 
        inet_ntoa(cin.sin_addr), 
        ntohs(cin.sin_port), 
        g_events[i].last_active, 
        i);
    return;
}

//读取客户端发来的数据--回调函数
void recvdata(int fd, int events, void* arg) {

    struct myevent_s *ev = (struct myevent_s*)arg;
    int len;

    len = recv(fd, ev->buf, sizeof(ev->buf), 0);

    eventdel(g_efd, ev);

    if(len > 0) {
        ev->len = len;
        ev->buf[len] = '\0';
        printf("cli[%d]:%s\n", ev->fd, ev->buf);

        eventset(ev, ev->fd, senddata, ev);

        eventadd(g_efd, EPOLLOUT, ev);
    }
    else if(len == 0) { //客户端关闭
        close(ev->fd);
        printf("cli[%d]: pos[%d], close.\n", fd, (int)(ev - g_events));
    }
    else { //客户端异常终止
        close(ev->fd);
        printf("recv[fd = %d] error[%d]:%s\n", fd, errno, strerror(errno));
    }

    return;
}

//向客户端发送数据--回调函数
void senddata(int fd, int events, void* arg) {
    struct myevent_s *ev = (struct myevent_s*)arg;
    int len;

    eventdel(g_efd, ev);

    len = send(fd, ev->buf, ev->len, 0);
    if(len > 0) {
        printf("send[fd = %d], [%d]%s\n", ev->fd, len, ev->buf);

        eventset(ev, ev->fd, recvdata, ev);

        eventadd(g_efd, EPOLLIN, ev);
    }
    else {
        close(ev->fd);
        printf("send[fd = %d] error[%d]:%s\n", fd, errno, strerror(errno));
    }

    return;
}

//设置socket--绑定--设置监听
void initListenSocket(int efd, int port) {
    struct sockaddr_in sin;

    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd < 0) {
        perror("socket error");
        exit(1);
    }
    int flag = fcntl(lfd, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(lfd, F_SETFL, flag);

    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(port);
    sin.sin_addr.s_addr = htonl(INADDR_ANY);

    int ret = bind(lfd, (struct sockaddr*)&sin, sizeof(sin));
    if(ret < 0) {
        perror("bind error");
        exit(1);
    }

    ret = listen(lfd, 128);
    if(ret < 0) {
        perror("listen error");
        exit(1);
    }

    //void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg)
    eventset(&g_events[MAX_EVENTS], lfd, acceptcon, &g_events);

    //void eventadd(int efd, int events, struct myevent_s *ev)
    eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
}

int main() {
    int port = SERV_PORT;

    g_efd = epoll_create(MAX_EVENTS);
    if(g_efd < 0) {
        perror("epoll_create error");
        exit(1);
    }

    //初始化socket,绑定,设置监听等
    initListenSocket(g_efd, port);

    struct epoll_event events[MAX_EVENTS + 1];
    printf("Sever Running:port[%d]\n", port);

    //int checkpos = 0;
    int i;
    while(1) {
        int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
        if(nfd < 0) {
            perror("epoll_wait error");
            exit(1);
        }

        for(i = 0; i < nfd; i++) {
            struct myevent_s *ev = (struct myevent_s*)events[i].data.ptr;

            if((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }

            if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }
        }
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值