• linux C/C++ socket编程


    前言

    我们都知道socket编程实际上是使用tcp或者udp协议进行消息传输,所以我们要更为的了解tcp/udp协议
    tcp三次握手
    先看tcp的三次握手示意图
    在这里插入图片描述
    在这里插入图片描述
    TCP 状态转换图
    在这里插入图片描述

    linux socket和tcp的关系
    在这里插入图片描述

    linux socket api介绍

    首先我们要先#include头文件,我们进行socket编程先#include #include 他们作为socket函数等必要使用的头文件,我们还要一些结构体存储ip地址等等信息,所以我们还要#include

    我们编写socket程序要先创建一个socket,socket其实就是一个int

    什么是socket?其实socket是一个fd,fd可以说是linux一切皆文件的精髓,linux外部设备可以抽象成fd,比如socket,其本质是网络fd,fd是用户空间和网络空间的一个接口,用户每打开一个文件(unix一切皆文件就会返回一个独一无二的fd),用户看fd就是一个int类型的数字,从内核空间看,我们打开一个文件,回返回一个fd,并且会在global file table中创建一个表项,其每个表项包含此fd的读取限制,偏移量,指向的inode
    在这里插入图片描述
    关于inode可以看这里

    创建socket
    我们一般用socket()函数创建socket,使用示例如下

    socket(DOMAIN, SOCKET_TYPE, PROTOCOL);
    
    • 1

    DOMAIN:指的是我们使用什么协议族,比如UNIX本地传输(AF_UNIX),比如隧道(AF_PPPOX),比如Infiniband(AF_IB),蓝牙(AF_BLUETOOTH),比如IPV4(AF_INET),比如IPV6(AF_INET6),IPV4,IPV6一般限定在传输层,如果向处理二层等低层次报文我们可以用(AF_PACKET)
    SOCKET_TYPE:指的是我们socket类型(相较于DOMAIN更具体),我们可以使用tcp/ip类型的socket(SOCK_STREAM),也可以使用UDP(SOCK_DGRAM),也可以使用raw socket,一旦使用raw socket我们就把数据链路层的包头移除自己写(rawsocket一般用于二层网路编程)(SOCK_RAW)
    protocol:protocal一般是指一些socket可以进行特殊的socket配置,如果不进行特殊配置我们就置为0

    我们创建一个简单的tcp socket(四层,ip是三层),然后如下,第一个参数指定网络层协议(或者二层协议,但这个时候要用到sock_raw),第二个参数指定网络层之上的协议,第三个如果不是sock_raw就不用指定

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    
    • 1

    因为我们上面创建的tcp socket传输到4层已经没有前面的二层帧头,所以我们为了让我们的socket能处理二层也就是数据链路层的报文,就创建raw socket,如下

    int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    
    • 1

    ETH_P_ALL是一个宏,代表接收所有二层以太网包

    创建ip/port结构
    我们要访问对方,要有对方的ip和端口,所以我们要创建地址相关的结构,这个结构就是sockaddr_in

    struct sockaddr_in server_address;
    
    • 1

    我们设置这个地址的地址组(ipv4)

    server_address.sin_family = AF_INET;
    
    • 1

    我们设置这个地址的端口

    server_address.sin_port = htons(9002);
    
    • 1

    设置对端的地址的ip

    server_address.sin_addr.s_addr = INADDR_ANY;
    
    • 1

    INADDR_ANY是一个宏代表ip是任意ip,一般指的是本机的任意ip

    还有一种表示ip的方法是用inet_addr()函数,这个函数输入一个ip地址的字符串(xxx.xxx.xxx.xxx),然后inet_addr()将这个ip地址的字符串其转换成int类型输出,怎么转换,主要是讲ip地址转换成二进制合并在一起(小端法),怎么使用这个int类型的ip地址呢?将其转换成16进制,然后每2个16进制位代表一个xxx(因为2个16进制最大ff转换成二进制是255)

    connect服务端
    我们知道我们有了socket,有了服务端的地址,我们可以使用connect连接服务端了
    connect函数如下

    connect(SOCKET, ADDR, ADDR_LEN);
    
    • 1

    上述三个参数都好理解,注意的是我们上面第二个参数指的是sockkaddr,我们要将我们的sockaddr_in转换成sockaddr,我们直接根据上述的例子接着写

    int conn_status = connect(sock, (struct sockaddr* )&server_address, sizeof(server_address));
    
    • 1

    连接可能出错,假如connect返回-1说明连接出错,我们要侦测这个错误,用perror侦测错误码

    if(conn_status == -1){
    	perror("connect error because: ");
    	close(sock);
    	exit(EXIT_FAILURE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    传输数据
    connect成功后我们就已经完成了三次握手,所以此时我们需要的是和服务端传输数据,比如向服务端发送数据,或者从服务端接收数据,我们就假定从服务端接收数据,用recv,接收数据的时候我们要设定接收数据的缓冲区
    因为传输中容易出错,而出错后会写入errno中,recv常见的错误如下
    在这里插入图片描述

    发送缓冲区故障和copy的时候出现故障都会返回SOCKET_ERROR,且设置errno

    我们先看recv函数,recvblock

    recv(SOCKET, &BUFFER, BUFFER_LEN, flag);
    
    • 1

    前三个参数都好理解,最后一个flag有啥呢?太多了直接man recv看吧,里面有设置成non-block等等
    代码示例如下

    char recv_buffer[255];
    recv(sock, &recv_buffer, stizeof(recv_buffer), 0);
    //print recive
    printf("the server sent the data is: %s\", recv_buffer);
    
    • 1
    • 2
    • 3
    • 4

    发送数据我们用send

    send(sockfd, &sendbuf, sendbuflen, flag);
    
    • 1

    bind
    这个用于服务端,将socket和本地地址端口进行bind,以方便后面监听

    bind(SOCKET, struct sockaddr* ADDR, ADDR_LEN);
    
    • 1

    上面三个参数就不用介绍了吧

    listen
    listen后server就监听在这个端口上,这里主要是配置半连接(backlog)等东西

    我们的tcp三次握手是由客户端发起的,客户端最开始是CLOSE状态,发送完syn给服务端后,客户端的状态变成SYN_SEND,服务端一开始也是CLOSE状态,收到客户端发送的syn后变成SYN_RECV,服务端在此时会将这个连接放入半连接队列中(syn queue),我们可以通过ss -i查看半连接队列大小, 在服务端将ACK(客户端SYN+1)和服务端的SYN发送给客户端后,客户端接收服务端的ACK(客户端SYN+1)和服务端的SYN发送给客户端后客户端变成ESTABLISH,然后客户端发送ACK(服务端SYN+1)给服务端,服务端收到客户端的ACK后也变成ESTABLISH,并且将这个连接从半连接队列中拿出到全连接队列中

    listen的函数如下

    listen(socket, backlog);
    
    • 1

    backlog是个啥?如下
    backlog is the number of connections allowed on the incoming queue. What does that mean? Well, incoming connections are going to wait in this queue until you accept() them
    换句话说我们listen后,linux的tcp/ip协议栈已经开始开始处理tcp连接,等请求到达,服务端后,服务端先缓存起来(缓存到半连接队列,此时服务端的状态是SYN_REVIC),然后进行正常的三次握手,最后都服务端成为ESTABLISH状态后将半连接队列对应的数据移动到全连接队列,最后accpet函数返回一个新的socket给客户端,也就是说我们accept发生在三次握手之后(服务端和客户端都已经成ESTABLISH的状态),backlog也就是我们的全连接队列大小,我们当然也可以通过设置/proc/sys/net/core/somaxconn,当然这个是设置上限

    accept
    这个只是将新的socket发送给客户端(连接已经被缓存在全连接队列中),函数如下

    accept(sockfd, struct sockaddr*, addrlen)
    
    • 1

    第二个和第三个参数可以为NULL,因为我们可以不用特别指定对端的客户端ip是多少,accept返回客户端的socket

    设置socket block和no block

    int fcntl(int fd, int cmd,...);
    
    • 1

    fcnt是设置fd属性的,我们的socket在linux中是一个fd,这是毋庸置疑的,首先设置flag,在设置flag的参数,比如我们可以设置fd的fd flag(F_SETFD),设置fd的file statue flag(F_SETFL),比如我们设置fd为no_block如下

    fcntl(sock, F_SETFL, O_NONBLOCK);
    
    • 1

    我们也可以先将当前fd的flag先取出来再设置

    int flags = fcntl(fd, F_GETFL, 0);//取出当前的flag
    if(flags < 0){ //如果fcntl因为fd或者某些原因出错
    	(void)close(fd);
    }
    flags |= O_NONBLOCK;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果我们将fd设置为非阻塞后,假设数据还没有经过内核到用户空间,此时我们read或者其他方式操作这个noblock的fd,会发生错误(errno),且errno会设置成EAGAIN,意思是等会再试,所以用noblock的时候一般用轮询(while),但是这样耗费cpu资源

    select
    select就是选择多个fd进行监听,如果那个fd有情况就返回,select也可以阻塞和非阻塞
    int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

    nfd代表我们要监听的fd范围[0,nfd),
    readfds一旦我们的fd集合中有情况可以读这里都会返回
    writefds同上
    exceptfds同上不过这里是如果fd有错误
    timeout因为select会阻塞,这里设置阻塞时间,如果超过这个时间还没有fd有反应就返回

    include <sys/select.h>
    #include 
    //#include 
    
    int main(void){
    	fd_set rd; //设置fd集合
    	struct timeval tv; //设置select阻塞的时间
    
    	FD_ZERO(&rd); //初始化fd集合
    	FD_SET(0, &rd); //将我们待监听的fd 0(标准输入)注册到rd这个fd集合中
    
    	tv.tv_sec = 5;  //调用select的时候timeout 5秒,5秒后如果select没有收到任何一个注册的fd数据到达的信号,就退出阻塞
    	tv.tv_usec = 0;
    	int err = select(1, &rd, NULL, NULL, &tv); //block
    	
    	if(err == 0){
    		printf("select time out!!!\n");
    	}else if(err == -1){
    		printf("select error!!!\n");
    	}else if(err >0){
    		printf("select success!!!\n");
    	}
    
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    简单客户端例子

    #include 
    #include 
    #include 
    
    #include 
    #include 
    #include 
    
    #include 
    
    #include 
    
    int main(void){
            //create sock
            int sock = socket(AF_INET, SOCK_STREAM, 0);
            if(sock < 0){
                    fprintf(stderr, "can't create socket because of %s\n", strerror(errno));
                    exit(EXIT_FAILURE);
            }
    
            //create address
            struct sockaddr_in server_addr;
            server_addr.sin_addr.s_addr = INADDR_ANY;
            server_addr.sin_port = htons(9001);
            server_addr.sin_family = AF_INET;
    
            //connect
            if( (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr))) == -1  ){
                    perror("connect error because of: ");
                    close(sock);
                    exit(EXIT_FAILURE);
    
            }
    
            //create recv buffer and recv
            char recv_buffer[255];
            recv(sock, recv_buffer, sizeof(recv_buffer), 0);
    
            //print
            printf("printf recv: %s\n", recv_buffer);
    
            close(sock);
            return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    简单的服务端

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include 
    
    
    int
    main(void){
            char send_message[255] = "hello i am server";
            int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    
            // address
            struct sockaddr_in server_addr;
            server_addr.sin_addr.s_addr = INADDR_ANY;
            server_addr.sin_port = htons(9001);
            server_addr.sin_family = AF_INET; 
    
            //bind
            if ( (bind(server_sock, (struct sockaddr*) &server_addr, sizeof(server_addr))) != 0 ){
                    fprintf(stderr, "couldn't bind: %s\n", strerror(errno));
                    exit(EXIT_FAILURE);
            }
    
            //listen
            if ((listen(server_sock, 10)) != 0){
                    fprintf(stderr, "couldn't listen: %s\n", strerror(errno));
                    exit(EXIT_FAILURE);
            }
    
            int client_sock;
            //accept
            if( (client_sock = accept(server_sock, NULL, NULL)) == -1 ){
                    fprintf(stderr, "couldn't accept: %s\n", strerror(errno));
                    exit(EXIT_FAILURE);
            }
    
            send(client_sock, send_message, sizeof(send_message), 0);
    
            close(server_sock);
            close(client_sock);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    上述程序的问题

    我们的服务端起来后会在accept这里block住,但是当我们crtl + c强制退出的时候,listen的端口还会继续占用
    这个可以在我们的状态图中找到答案
    首先我们正常的四次挥手是客户端发起的(连接也是客户端发起的),如下
    在这里插入图片描述

    epoll

    在中文互联网中关于epoll的介绍太少,要么就是上来直接内核源码分析,要么上来直接放代码,虽然epoll操作就那么几个函数,但是代码中完全不讲怎么设计,为什么这么使用,本篇教程主要从头开始细讲epoll使用

    首先epoll这个技术早在2000年前后就提出,他是使用IO复用技术解决高并发问题

    正对高并发业务(C!0K)我们首先理想的解决方法是服务段为每一个连接创建一个线程去处理,但是假设在C10K场景下,我们一个进程要创建10000个线程,虽然在OS理论中创建10000个线程是没问题的(linux中可能要设置内核参数),但是想过这个问题没有,我们的线程是不是也要进行上下文切换(CPU Core上的线程就那么多),虽然线程的上下文切换没有进程那么复杂且繁重,但是你也要保存SP,PC,等等寄存器信息,10000个线程(操作系统概念的线程)在CPU Core(硬件线程,就是我们操作系统线程最终正真映射,运行的位置)上仅有的那么几个线程上来回切换,想想效率就不高,并且最重要的我们一个进程有10000个线程意味着我们进程的地址空间的stack区域分成10000份(一个进程中的线程共享进程地址空间的data,heap,code区域),其中某个线程处理数据过大极有可能超出他应该在的stack区域,到其他线程的stack区域中,所以这个方案不是非常好
    而epoll就有点像事件驱动io(我不知道他是不是事件驱动io),关于io的5大分类分别是阻塞io,非阻塞io,io复用,事件驱动io,异步io,如果想了解这方面的只是请看这里

    epoll到底是怎么做到一个线程监控多个客户端fd呢?首先我们都知道unix一切皆文件这个哲学,unix对网络连接抽象成socket文件(socket fd),那么数以万计的client连接过来在unix中都是client socket fd(本质还是文件),我们在使用epoll之初先创建一个epoll fd(epoll_create())这个epoll fd指向的文件是一个红黑树,当我们想监听某个client socket fd就讲这个client socket fd放入这个红黑树中,这样做到了一个epoll fd监听多个client socket fd
    但是我们不可能去每时每刻的轮询红黑树中的所有socket(效率太差),所以我们的解决方法是epoll fd监听的socket中,那个socket触发了预先设置绑定的事件(epoll_ctl()),那么那个socket fd就返回通知epollfd(这里都是内核在通知),假设有太多太多的socket通知epollfd,epollfd不会要一个一个回应把,这里不是的因为我们在epoll_wait()使某个epollfd开始阻塞监听的时候会传入一个epoll_event结构数组,当有一个socket被触发就将这个socket的信息覆写到epoll_event结构数组的某个成员中,所以我们后面确定哪个socket返回了数据就看epoll_event.data.fd

    下面的示例代码我们用epoll的edge-trigger来写(level trigger不会),因为edge-trigger代表当被监听的socket因为某个预先注册的事件被触发而notification epollfd(将这个被触发的socket的对应信息写入epoll_event.data.fd中),这个notification在edge-trigger中只有一次,所以我们处理数据的时候要尽量处理完,因为我们设置事件的时候可能触发事件为EPOLLIN代表socket有数据来我们就触发notification,不管数据有没有发送完毕,所以我们处理数据的时候最好把read()放在while中读取

    下面是代码

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define MAXBUF 1024
    #define CLIENT_ADDR_NAME 20
    #define MAXEVENT 64
    
    void set_noblocking(int  fd){
    	int flags,s;
    	flags = fcntl(fd, F_GETFL, 0);
    	if(flags == -1){
    		fprintf(stderr, "couldn't fcntl: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	flags |= O_NONBLOCK;
    	if((s = fcntl(fd, F_SETFL, flags)) == -1){
    		fprintf(stderr, "couldn't fcntl: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    }
    
    int setup_epoll_et(int epollfd, int serverfd, struct epoll_event * ev){
    	ev->data.fd = serverfd; //代表我们notification这个fd发来的event
    	ev->events  = EPOLLIN|EPOLLET;//设置水平触发(et),还有水平触发的事件是EPOLLIN,当有消息到这个fd的时候就触发notification提醒epoll_wait,但是水平触发只提醒一次也就是我们设置的事件发生的时候提醒 
    	//set epoll to edge-trriger
    	if((epoll_ctl(epollfd, EPOLL_CTL_ADD, serverfd, ev)) == -1){
    		fprintf(stderr, "couldn't set epoll: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	
    	return epollfd;
    
    
    }
    
    void http_setup(struct sockaddr_in* server_addr){
    	int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    	set_noblocking(server_sock);
    	int opt = 1;
    	setsockopt(server_sock, SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt) ); // avoid time_wait
    	//bind
    	if ( (bind(server_sock, (struct sockaddr*) server_addr, sizeof(struct sockaddr_in))) != 0 ){
    		fprintf(stderr, "couldn't bind: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    
    	//listen
    	if ((listen(server_sock, 10)) != 0){
    		fprintf(stderr, "couldn't listen: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	//set epoll
    	struct epoll_event  ev;
    	struct epoll_event* events;
    	int epollfd;
    	//create epoll fd
    	if((epollfd = epoll_create1(0)) == -1){
    		fprintf(stderr, "couldn't createpoll fd: %s\n", strerror(errno));
    		exit(EXIT_FAILURE);
    	}
    	epollfd = setup_epoll_et(epollfd, server_sock, &ev);
    		
    	events = malloc(sizeof(ev) * MAXEVENT);
    
    
    	while(1){
    		int client_sock;
    		int rval;
    		//char buf[MAXBUF];
    
    		struct sockaddr_in client_addr;
    		char client_addr_name[CLIENT_ADDR_NAME];
    		socklen_t length; 
    		
    		int i,n;
    		//memset(buf, 0, sizeof(buf));
    		//printf("break point1\n");
    		n = epoll_wait(epollfd, events, MAXEVENT, -1);
    		//printf("break point2\n");
    		
    		for(i = 0; i < n; i++){ //n代表epollfd中注册的fd被触发了某个事件,这个事件是我们预先确定,这个事件出现就notification
    			if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)) ){
    				printf("fd error,or socket not ready to reading%s\n");
    				close(events[i].data.fd);
    				continue;
    			}else if (events[i].data.fd == server_sock){
    				//accept
    				//printf("break point3\n");
    				if( (client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &length)) == -1 ){
    					fprintf(stderr, "couldn't accept: %s\n", strerror(errno));
    					exit(EXIT_FAILURE);
    				}
    				/*make client socket not  blocking*/
    				set_noblocking(client_sock);
    				/*给client_socket设置epoll的边缘触发,因为给client_socket注册边缘触发到epollfd中后,client_socket被notification(只因为数据来了)那么events[]结构中的某些数据
    				会被内核覆盖比如client_sock的fd(覆盖到events[i].data.fd)中,这样我们下面的else就可以直接处理这个数据,因为else代表没出错,不是server socket,那么就一定是
    				client socket,一个epollfd可以监听多个socket,当被触发他们都被写入到events[]中*/
    				ev.data.fd = client_sock;
    				ev.events = EPOLLIN|EPOLLET;
    				setup_epoll_et(epollfd, client_sock, &ev);/*将client_sock注册到epollfd中,这样后面的(处于循环中)epoll_wait(epollfd,...)就可以接受到因为关于client_sock触发
    									    event而发送过来的notification*/
    
    			}else{ //不是server_sock,没有出错,那么就一定是client_sock
    				int done = 0; //if down the socket
    				char buf[MAXBUF] = {0};
    				while(1){
    					int n = read(events[i].data.fd, buf, MAXBUF);
    					if(n < 0){
    						if(errno == EAGAIN){
    							 //errno = EAGAIN
    							 //EAGAIN代表socket想阻塞(因为有数据来了)但是我们因为设置了socket no-block所以会报出这个错误,但是这里不是错误而是一种信号说明数据来了,那么我们退出while读数据
    							break; 
    
    						}else{ 
    							fprintf(stderr, "read error:%s\n", strerror(errno));
    							done = 1;
    							break;
    						}
    					}else if(n == 0){ //end of the file,在netowrk socket中得到eof表明对端(peer,这里是client_sock)关闭了连接
    						done = 1;
    						break;
    					}
    					
    				}
    				if(done) {
    					close(events[i].data.fd);
    					inet_ntop(AF_INET, &client_addr.sin_addr, client_addr_name, CLIENT_ADDR_NAME);
    					printf("client: %s connect quit\n", client_addr_name);
    					continue;
    				}
    				inet_ntop(AF_INET, &client_addr.sin_addr, client_addr_name, CLIENT_ADDR_NAME);
    				printf("client:%s, send:%s\n", client_addr_name, buf);
    			}
    		}
    	
    	}
    	close(server_sock);
    }
    
    int
    main(void){
    	char send_message[255] = "hello i am server";
    	
    	// address1
    	struct sockaddr_in server_addr1;
    	server_addr1.sin_addr.s_addr = INADDR_ANY;
    	inet_pton(AF_INET, "192.168.152.215", &server_addr1.sin_addr); //将特定地址给sockaddr_in
    	server_addr1.sin_port = htons(9001);
    	server_addr1.sin_family = AF_INET; 
    
    	http_setup(&server_addr1);
    	return 0;
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162

    我们实现非常简单,用epoll的方式同时接收多个client 发送过来的内容打印在server的屏幕上

    对于epoll部分,我们先用epoll_create1()创建了一个epollfd,用epoll_ctr()要epollfd监听我们需要被监听的fd,并且设置边缘触发和触发事件(设置边缘触发和触发事件需压写入epoll_event然后再当参数传进epoll_ctl()具体看函数setup_epoll_et())

    然后我们使用epoll_wait()监听epollfd,第二个参数events上面讲了用于将触发事件的socket信息(fd等)放到预先定义好的epoll_event结构数组中,至于为什么要将epoll_wait()放到一个循环中,因为我要一直监听,epoll_wait()随后的是一个for循环,这里最为重要,上一句讲了epoll_wait()的第二个参数用于接收触发事件的socket信息,那么这里我们遍历这个epoll_event结构数组,看他的fd是否等于我们的server fd,如果等于说明有请求(三次握手)到达我们的服务端,然后accept()并且再将accept()返回的client socket fd注册进epollfd中(epoll_ctr())且设置成边缘触发,最后如果epoll_event结构数组的元素的fd不等于serverfd,且没有出错那么就等于我们刚刚注册的client sock fd,此时我们就开始进行处理来的数据

    处理进来的数据的时候还有一些坑,因为我们之前设置client sock fd为非阻塞,如果数据来了之后在read()的时候,kernel会请求阻塞,但是我们设置了非阻塞,那么就会报错报错返回EAGAIN,我们要对他进行特殊处理,因为在我们的这个场景中他不再是报错,而是提醒我们数据来了,我们将数据读出打印

    raw socket

    首先socket分为2种,分别是non-raw socket和raw socket,non-row socket意味着我们只能操作数据包的payload,且只能操作传输层及以上的数据包,如果是raw socket,我们既可以操作payload和header,并且可以操作二层(AF_PACKET),三层(AF_INET),等数据帧/包

    • socket(AF_INET,RAW_SOCKET,...)means L3 socket , Network Layer Protocol = IPv4
    • socket(AF_IPX,RAW_SOCKET,...)means L3 socket , Network Layer Protocol = IPX
    • socket(AF_INET6,RAW_SOCKET,...)means L3 socket , Network Layer Protocol=IPv6
    • socket(AF_PACKET,RAW_SOCKET,...)means L2 socket , Data-link Layer Protocol= Ethernet

    我们写一个ping程序,使用row socket探测icmp(网络层)的包头

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    //checksum算法抄别人的...
    unsigned short checksum(unsigned short *buf, int bufsz){
        unsigned long sum = 0xffff;
    
        while(bufsz > 1){
            sum += *buf;
            buf++;
            bufsz -= 2;
        }
    
        if(bufsz == 1)
            sum += *(unsigned char*)buf;
    
        sum = (sum & 0xffff) + (sum >> 16);
        sum = (sum & 0xffff) + (sum >> 16);
    
        return ~sum;
    }
    
    
    int main(int argc, char** argv){
        if(argc != 2){
            fprintf(stderr, "argc != 2 , format: ping IP :%s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
    
        int icmp_socket;
        struct sockaddr_in addr;
        struct icmphdr hdr;
    
        struct iphdr * iphdr_for_reciver; //for revicer
        struct icmphdr * icmphdr_for_reciver; //for revicer
        char buf[1024] = {0};
        if((icmp_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0 ){
            fprintf(stderr, "can't create socket:\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
    
        addr.sin_family = AF_INET;
        inet_pton(AF_INET, argv[1],&addr.sin_addr); //将目标ip传入
        //init icmp header
        //|---type---|---code---|-------check_sum------|
        //    1Byte      1Byte          2Byte
        //|--------------------unused------------------|
        //                     4Byte
        hdr.type = ICMP_ECHO;  //8代表echo request,具体看wiki
        hdr.code = 0;//看wiki
        hdr.checksum = 0;
        //sequence用于确定icmp包的顺序,因为在网络传输中先发出的包可能后到,这个时候clinet需要通过这个确定顺序,我们只是测试所以都设置为0把
        hdr.un.echo.sequence = 0; 
        hdr.un.echo.id = 0;
    
        //设置checksum,前面只是初始化
        hdr.checksum = checksum((unsigned short*)&hdr, sizeof(hdr));
    
        //使用sendto发送
        //sendto和send都是用来发送,通常send用于tcp,sendto用于udp,但是tcp因为有连接的,所以发送的过程中不需要操心太多事情,而udp则时无连接的,所以发送udp包的时候需要指定很多选项而sendto就提供了这个功能,我们发送icmp包也需要指定非常多选项(指定peer地址,send则不需要)所以也用sendto
        sendto(icmp_socket, (char *)&hdr, sizeof(hdr), 0, (struct sockaddr*)&addr, sizeof(addr));
        if((sendto(icmp_socket, (char *)&hdr, sizeof(hdr), 0, (struct sockaddr*)&addr, sizeof(addr))) == -1){
            fprintf(stderr, "sendto error! :%s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
    
        printf("sending icmp packet to peer %s\n", argv[1]);
    
        //recive
        if(recv(icmp_socket, buf, sizeof(buf), 0) == -1 ){
            fprintf(stderr, "recive error! :%s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
        //取出ip包头
        iphdr_for_reciver = (struct iphdr*)buf;
        //取出icmp包头
        icmphdr_for_reciver = (struct icmphdr*)(buf+(iphdr_for_reciver->ihl)*4); //ihl是ip包头的字段代表internet header length
    
        switch(icmphdr_for_reciver->type){ //看wiki
            case 3:
                printf("hosts %s is unreachable \n", argv[1]);
                printf("icmp respond header code is %d\n", icmphdr_for_reciver->code);
                break;
            case 0:
                printf("hosts %s is alive \n", argv[1]);
                printf("icmp respond header code is %d\n", icmphdr_for_reciver->code);
                break;
            default:
                printf("unknow satuation, icmp respond header type is %d code is %d\n", icmphdr_for_reciver->type, icmphdr_for_reciver->code);
    
        close(icmp_socket);
    
        return 0;
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
  • 相关阅读:
    LeetCode --- 145. Binary Tree Postorder Traversal 解题报告
    电脑蓝屏怎么办 七大原因及解决办法来帮你
    物流通知:您的快递即刻送达!
    为什么我们如此热爱Python
    ALINX_ZYNQ_MPSoC开发平台FPGA教程:PL的点灯实验
    MySQL允许root远程登录
    VirtualBox启动问题记录
    springMvc22-eclipse创建Maven项目没有src/main/java并不能新建的问题
    vue打印功能
    @Component与@Configuration区别
  • 原文地址:https://blog.csdn.net/qq_37026934/article/details/126405743