网络编程:1. 网络IO与select

网络编程:网络IO与select

1. 网络IO是什么?

IO的含义是输入输出(input/output)

网络IO本质上也是IO的一种,涉及到两个系统的对象

  1. 用户空间调用IO的进程或者线程
  2. 内核空间的内核系统

2. socket是什么?

socket套接字是一种通信机制,提供了TCP/IP协议的抽象,对外提供了一套接口,通过这个接口可以统一、方便的使用TCP/IP协议的功能

3. FD是什么?

FD(File descriptor),文件描述符是一个非负整数,本质上是一个索引值

FD的取值范围:0~OPEN_MAX - 1

  • 在POSIX的语义中0,1,2这三个fd值被分别赋予为标准输入(STDIN_FILENO),标准输出(STDOUT_FILENO),标准错误(STDERR_FILENO)
  • 最大打开文件数通过ulimit命令查看,例如:ulimit -n

当打开一个文件时,内核向进程返回一个文件描述符(open系统调用得到),后续read、write这个文件时,只需要用这个文件描述符来标识该文件,将其作为参数传入read、write就能读写文件

4. Linux网络连接(单连接)

下面是最基础的TCP端口监听代码,默认创建socket的listenfd是阻塞的,如果需要变成非阻塞的,需要使用fcntl这个函数去对listenfd做或操作(位运算)

下面的代码现在只能与一个客户端建立连接(只能连接一次),可以多客户端连接,但是只会接收第一个连接进来的客户端的信息

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>

#define BUFFER_LENGTH 128

int main(){
    
    
    // 1. 使用socket套接字创建fd, fd默认是阻塞的
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
    
    
        printf("create listend fd failed");
        return -1;
    }
    // 2. listenfd绑定一个固定的地址
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
		// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
    
    
        printf("bind failed");
        return -2;
    }
		// 修改fd的性质, 改为非阻塞
		// int flag = fcntl(listenfd, F_GETFL, 0);
    // flag |= O_NONBLOCK; // 如果直接set可能会把原来已有的属性给覆盖掉,所以需要或
    // fcntl(listenfd, F_SETFL, flag);
		// 3. 新建一个监听队列
		// Prepare to accept connections on socket FD.
	  // N connection requests will be queued before further requests are refused.
		listen(listenfd, 10);
		// 4. 新建一个连接客户端的fd
    /* Structure describing an Internet socket address.  */
		struct sockaddr_in client;
    socklen_t len = sizeof(client);
    // 此时listenfd是一个阻塞的, 因为新建socket的时候默认就是阻塞的
		// Await a connection on socket FD.
    // When a connection arrives, open a new socket to communicate with it
    int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
    while(1){
    
    
				// 4.1 新建一个接收缓冲区
        unsigned char buffer[BUFFER_LENGTH] = {
    
    0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        printf("buffer: %s, ret: %d\n", buffer, ret);
				// Send N bytes of BUF to socket FD.  Returns the number sent or -1.
        ret = send(clientfd, buffer, ret, 0);
    }

5. Linux网络连接(多线程)

为了解决上面的只能和第一个客户端通信的问题,使用多线程的方式改进

使用多线程处理多客户端连接,会引出一个问题,如果1万个客户端连接,就得新建1万个线程,这个开销是有点大的

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>

#define BUFFER_LENGTH 128

void *routine(void *arg){
    
    
    int clientfd = *(int *)arg;
    while(1){
    
    
        unsigned char buffer[BUFFER_LENGTH] = {
    
    0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        // 关闭客户端
        if(ret == 0){
    
    
            close(clientfd);
            break;
        }
        printf("buffer: %s, ret: %d\n", buffer, ret);

        ret = send(clientfd, buffer, ret, 0);
    }
}

int main(){
    
    
    // 1. 使用socket套接字创建fd, fd默认是阻塞的
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
    
    
        printf("create listend fd failed");
        return -1;
    }
    // 2. listenfd绑定一个固定的地址
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
		// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
    
    
        printf("bind failed");
        return -2;
    }
		// 3. 新建一个监听队列
		listen(listenfd, 10);
		while(1){
    
    
				// 4. 新建连接客户端的fd
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
	      // 创建线程去处理客户端fd发来的请求
        pthread_t threadid;
        pthread_create(&threadid, NULL, routine, &clientfd);
    }

6. IO多路复用

IO多路复用是网络编程中常提到的select/epoll,也称为事件驱动IO(event driven IO),作用是检测IO是否有事件

  • 优点:单个进程可以同时处理多个网络连接的IO(轮询)
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>

#define BUFFER_LENGTH 128

int main(){
    
    
    // 1. 使用socket套接字创建fd, fd默认是阻塞的
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) {
    
    
        printf("create listend fd failed");
        return -1;
    }
    // 2. listenfd绑定一个固定的地址
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);
		// 2.1 将listenfd绑定协议簇, 端口9999, 这样listenfd就会一直监听9999端口的信息
    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
    
    
        printf("bind failed");
        return -2;
    }
		listen(listenfd, 10);
		// 3. 定义读事件和写事件
		fd_set rfds, wfds, rset, wset;
		// 3.1 清空readfd集合中所有bit, 置为0
    FD_ZERO(&rfds); 
		// 3.2 将listenfd加入到fd读集合中
    FD_SET(listenfd, &rfds);
		// 一定要比实际在用的fd大
    int maxfd = listenfd;
    int ret = 0;
		unsigned char buffer[BUFFER_LENGTH] = {
    
    0};
		while(1) {
    
    
				// 3.3 把判断位 赋值给 修改位
        rset = rfds;
        wset = wfds;
        //           可读集合 可写集合
        // select其实是一个for循环
        // 返回实时的有多少个
        int nready = select(maxfd + 1, &rset, &wset, NULL, NULL);
        // 判断fd在不在rset中, rset是一个二进制位, 就看有没有这个位
        if(FD_ISSET(listenfd, &rset)){
    
    
            printf("listenfd --> \n");

            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 新增加的fd, 也加到可读的fd集合中
            int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
            FD_SET(clientfd, &rfds);
            if(clientfd > maxfd) maxfd = clientfd;
        }
				int i = 0;
        // 4. 由于fd是依次增加的, 从listenfd开始遍历, 一直到最大的fd
        for(i = listenfd + 1; i <= maxfd; i++){
    
    
						// 4.1 判断当前fd是不是在读集合
            if(FD_ISSET(i, &rset)){
    
    
								// 4.2 读取这个fd的缓冲区
                ret = recv(i, buffer, BUFFER_LENGTH, 0);
                // 4.3 如果recv的返回是0,表面客户端已经关闭了
                if(ret == 0){
    
    
                    close(i);
                    // 4.4 再把当前fd的事件clear掉
                    FD_CLR(i, &rfds);
                }else if(ret > 0){
    
    
										// 4.5 从缓冲区中读到了数据
                    printf("buffer: %s, ret: %d\n", buffer, ret);
										// 4.6 将当前fd加入到写集合中
                    FD_SET(i, &wfds);
										// FD_CLR(i, &rfds);  这行要注释吗?
                }
            }
						// 5. 如果上面的fd读取到了内容
            if(FD_ISSET(i, &wset)){
    
    
								// 5.1 发送数据到客户端
                ret = send(i, buffer, ret, 0);
                FD_CLR(i, &wfds);
                FD_SET(i, &rfds);
            }
        }
		}
}

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

猜你喜欢

转载自blog.csdn.net/weixin_44839362/article/details/128977062