linux的网络编程是基与socket编程的;首先总结socket编程;基于socket编程实现TCP通信的多并发的实现方式:多进程,多线程,select,poll,epoll,;这几种方式的如何实现,异同优缺点;最后介绍一下libevent框架;在实现网络编程的过程中,以上这些方式是基础的方法,在实现网络编程一般采用的是epoll+多线程;这是最高效的一种方式;
七层面试需要,4层做项目需要
数据的网络传输在物理(在咱们真实的世界)中其实是一些信号的传输,高低电频信号的传输;在计算机的世界里就是一些二进制01数字的传输;但是在传输的时候我们需要知道传输的目的地;数据从一台电脑的一个程序发送到另外一台电脑的一个程序,需要保证数据的安全;同时保证发送给另外一台电脑后保证另外一台电脑可以分析这些01数据;所以双方需要都遵从一定的规定,从而双方约定规则(所谓的协议);数据从应用层到物理层被一层层协议进行包装(所谓的包装其实就是加协议的规定,这些规定也是一些二进制的标志位);在物理层中真实传输的数据其实比想要的数据大;接收的一方收到数据后就会将收到的数据根据协议把不是真实想要的数据一层层的去掉;最后咱们看到的就是想要的数据;整个过程和咱们日常生活中发快递很像;一个快递要发送给你;需要知道你的地址(ip)和你的联系方式(端口号);你的快递(数据)在发送的过程加了包装,包装再写上你的地址和联系方式;然后通过交通道路(物理光纤)送到你的地址,然后你自己把包装去掉得到了快递;
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。 protofamily: 即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。 协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。 type: 指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等 protocol: 就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议 0是默认前面的协议; 注意: 当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
2.int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 sockfd: 即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。 addr: 一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: addrlen:对应的是地址的长度。 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。 注意: 在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,
3.int listen(int sockfd, int backlog);
第一个参数即为要监听的socket描述字, 第二个参数为相应socket可以排队的最大连接个数。 socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。 调用了listen方法后,服务端就打开了三次握手的开关,能够处理来自客户端的SYN分节了,只要三次握手完成,客户端就会connect成功,而跟服务端调用accept没任何关系,accept只是去取已完成连接队列的对头项。(引自:https://blog.csdn.net/junjun150013652/article/details/37966901?utm_source=tuicool&utm_medium=referral)
4.int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字, 这个套接字是连接套接字。TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。 参数 sockfd 参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。 参数 addr 这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL 参数 len 如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
5.int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一个参数即为客户端的socket描述字 第二参数为服务器的socket地址 第三个参数为socket地址的长度 客户端通过调用connect函数来建立与TCP服务器的连接。
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。 一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。
连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号
多进程: 父进程的主要作用是创建监听文件描述符; 子进程主要用来通信; 每启动一个客户端,就创建一个进程; 原因:accept()函数在没有客户端发请求的时候,处于阻塞状态,所以下面的fork()不运行; fork()每次都会拷贝一份一模一样的内存,里面包含了通信文件描述符cfd;在通信的时候使用;这种机制会消耗很大的资源,每个客户端都需要拷贝一份新的内存,不推荐使用 ;
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <errno.h> // 信号捕捉函数 void recycleChild(int num) { // 资源回收 while(1) { int ret = waitpid(-1, NULL, WNOHANG); if(ret == -1) { printf("所有的子进程回收完毕!!!\n"); break; } else if(ret == 0) { printf("剩下的子进程都还活着!!!\n"); break; } else { printf("child die, pid = %d\n", ret); } } } int main() { // 1. 创建用于监听的套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(0); } // 2. 绑定 struct sockaddr_in addr; addr.sin_family = AF_INET; // ipv4 addr.sin_port = htons(8989); // 字节序应该是网络字节序 //inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr); addr.sin_addr.s_addr = INADDR_ANY; // == 0, 获取IP的操作交给了内核 int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind"); exit(0); } // 3.设置监听 ret = listen(fd, 100); if(ret == -1) { perror("listen"); exit(0); } // 注册新号捕捉 struct sigaction act; act.sa_flags = 0; act.sa_handler = recycleChild; sigemptyset(&act.sa_mask); sigaction(SIGCHLD, &act, NULL); while(1) { // 4. 等待, 接受连接请求 struct sockaddr_in addrCli; int len = sizeof(addrCli); printf("正在焦急等待客户端的连接...\n"); int connfd = accept(fd, (struct sockaddr*)&addrCli, &len); if(connfd == -1) { if(errno == EINTR) { continue; } perror("accept"); exit(0); } // 成功和客户端建立了连接 // 创建子进程 pid_t pid = fork(); if(pid == 0) { int num = 0; while(1) { // 读数据 char recvBuf[1024]; // 如果客户端没有发送数据, 默认阻塞 int ret = read(connfd, recvBuf, sizeof(recvBuf)); if(ret == -1) { perror("read"); break; } else if(ret == 0) { printf("客户端已经断开了连接...\n"); break; } else { // 打印客户端地址信息 char ip[32]; inet_ntop(AF_INET, &addrCli.sin_addr.s_addr, ip, sizeof(ip)); printf("client IP: %s, Port: %d\n", ip, ntohs(addrCli.sin_port)); printf("客户端说: %s\n", recvBuf); // 写数据 sprintf(recvBuf, "你好, 客户端 - %d\n", num++); write(connfd, recvBuf, strlen(recvBuf)+1); } } close(connfd); // 通信 // 退出当前子进程 exit(0); } } // 释放资源 close(fd); // 监听 return 0; }多线程:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <errno.h> #include <pthread.h> struct SockInfo { int fd; // 通信 pthread_t tid; // 线程ID struct sockaddr_in addr; // 地址信息 }; struct SockInfo infos[128]; void* working(void* arg) { while(1) { struct SockInfo* info = (struct SockInfo*)arg; // 接收数据 char buf[1024]; int ret = read(info->fd, buf, sizeof(buf)); if(ret == 0) { printf("客户端已经关闭连接...\n"); info->fd = -1; break; } else if(ret == -1) { printf("接收数据失败...\n"); info->fd = -1; break; } else { write(info->fd, buf, strlen(buf)+1); } } return NULL; } int main() { // 1. 创建用于监听的套接字 int fd = socket(AF_INET, SOCK_STREAM, 0); if(fd == -1) { perror("socket"); exit(0); } // 2. 绑定 struct sockaddr_in addr; addr.sin_family = AF_INET; // ipv4 addr.sin_port = htons(8989); // 字节序应该是网络字节序 //inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr); addr.sin_addr.s_addr = INADDR_ANY; // == 0, 获取IP的操作交给了内核 int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); if(ret == -1) { perror("bind"); exit(0); } // 3.设置监听 ret = listen(fd, 100); if(ret == -1) { perror("listen"); exit(0); } // 4. 等待, 接受连接请求 int len = sizeof(struct sockaddr); // 数据初始化 int max = sizeof(infos) / sizeof(infos[0]); for(int i=0; i<max; ++i) { bzero(&infos[i], sizeof(infos[i])); infos[i].fd = -1; infos[i].tid = -1; } printf("111111111111111\n"); // 父进程监听, 子进程通信 while(1) { // 创建子线程 struct SockInfo* pinfo; for(int i=0; i<max; ++i) { printf("fd = %d\n", infos[i].fd); if(infos[i].fd == -1) { pinfo = &infos[i]; printf("i = %d\n", i); break; } if(i == max-1) { sleep(1); i--; } } printf("xxxxxxx\n"); int connfd = accept(fd, (struct sockaddr*)&pinfo->addr, &len); printf("parent thread, connfd: %d\n", connfd); if(connfd == -1) { perror("accept"); exit(0); } pinfo->fd = connfd; pthread_create(&pinfo->tid, NULL, working, pinfo); pthread_detach(pinfo->tid); } // 释放资源 close(fd); // 监听 return 0; }sever:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <event2/event.h> int main() { // 1. 创建事件处理框架 struct event_base* base = event_base_new(); // 打印支持的IO转接函数 const char** method = event_get_supported_methods(); for(int i=0; method[i] != NULL; ++i) { printf("%s\n", method[i]); } printf("current method: %s\n", event_base_get_method(base)); // 创建子进程 pid_t pid = fork(); if(pid == 0) { // 子进程中event_base也会被复制,在使用这个base时候要重新初始化 event_reinit(base); } // 2. 释放资源 event_base_free(base); return 0; }client:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <event2/event.h> #include <event2/bufferevent.h> // read缓冲区的回调 void read_cb(struct bufferevent* bev, void* arg) { printf("arg value: %s\n", (char*)arg); // 读缓冲区的数据 char buf[128]; int len = bufferevent_read(bev, buf, sizeof(buf)); printf("read data: len = %d, str = %s\n", len, buf); // 回复数据 bufferevent_write(bev, buf, len); printf("数据发送完毕...\n"); } // 写缓冲区的回调 // 调用的时机: 写缓冲区中的数据被发送出去之后, 该函数被调用 void write_cb(struct bufferevent* bev, void* arg) { printf("arg value: %s\n", (char*)arg); printf("数据已经发送完毕...xxxxxxxxxxxx\n"); } // 事件回调 void events_cb(struct bufferevent* bev, short event, void* arg) { if(event & BEV_EVENT_ERROR) { printf("some error happened ...\n"); } else if(event & BEV_EVENT_EOF) { printf("server disconnect ...\n"); } // 终止连接 bufferevent_free(bev); } void send_msg(evutil_socket_t fd, short ev, void * arg) { // 将写入到终端的数据读出 char buf[128]; int len = read(fd, buf, sizeof(buf)); // 发送给服务器 struct bufferevent* bev = (struct bufferevent*)arg; bufferevent_write(bev, buf, len); } int main() { struct event_base * base = event_base_new(); // 1. 创建通信的套接字 struct bufferevent* bufev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE); // 2. 连接服务器 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(9898); // 服务器监听的端口 inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr); // 这个函数调用成功, == 服务器已经成功连接 bufferevent_socket_connect(bufev, (struct sockaddr*)&addr, sizeof(addr)); // 3. 通信 // 给bufferevent的缓冲区设置回调 bufferevent_setcb(bufev, read_cb, write_cb, events_cb, (void*)"hello, world"); bufferevent_enable(bufev, EV_READ); // 创建一个普通的输入事件 struct event* myev = event_new(base, STDIN_FILENO, EV_READ|EV_PERSIST, send_msg, bufev); event_add(myev, NULL); event_base_dispatch(base); event_free(myev); event_base_free(base); return 0; }