计算机网络基础论文,网络基础由什么组成
墨初 知识笔记 35阅读
下面介绍一个最最简单的服务程序的编写流程先按照顺序介绍各个函数的参数和使用。然后在第三节用一对简单的程序对客户端与服务端通信过程进行演示。
1、创建套接字// 头文件#include <sys/types.h>#include <sys/socket.h>int socket(int domain, int type, int protocol);/** 参数domain通讯协议族* PF_INET IPv4互联网协议族常用* PF_INET6 IPv6互联网协议族* PF_LOCAL 本地通信的协议族* PF_PACKET 内核底层的协议族* PF_IPX IPX Novell协议族* IPv6尚未普及其它的不常用*//** 参数type数据传输的类型* SOCK_STREAM 面向连接的socket 数据不会丢失、顺序不会错乱、双向通道* SOCK_DGRAM 无连接的socket 数据可能会丢失、顺序可能会错乱、传输效率更高*//** 参数protocol最终使用的协议* 在IPv4网络协议家族中* 数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP* 数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP* 本参数也可以填0编译器自动识别*//** socket返回值* 成功返回一个有效的socket失败返回-1errno被设置*/
2、端口复用 // 这个步骤非必须int opt 1;unsigned int len sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);

这里简单介绍下SO_REUSEADDR的使用场景
1、主动断开连方的socket1处于TIME_WAIT状态时新创建的socket2想要绑定相同于socket1的IP和端口就要设置SO_REUSEADDR。

2、SO_REUSEADDR允许同一端口上启动同一服务器的多个实例即多个进程。但每个实例绑定的IP是不能相同的。有多块网卡或用IP Alias机器可以试试。
3、SO_REUSEADDR允许单个进程绑定相同端口到多个socket上但每个socket绑定的IP不同。
4、SO_REUSEADDR允许完全相同的IP和端口重复绑定。但只用于UDP多播不用于TCP。
3、设置IP和端口// 头文件#include <netdb.h>// 申请变量用于存放协议、端口和IP地址struct sockaddr_in servaddr;// 初始化memset(&servaddr,0,sizeof(servaddr));// 设置协议族servaddr.sin_family AF_INET;// 设置IP本机的所有IP都可用servaddr.sin_addr.s_addr htonl(INADDR_ANY);// 指定用于监听的端口servaddr.sin_port htons(m_port);
4、绑定IP和端口 // 失败返回-1成功返回0// 注意第二个参数要强制转换类型int bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
5、开始监听客户端 // 参数s监听的描述符listen_fd// 参数backlog已经完成连接正等待应用程序接收的套接字队列长度linux中// 失败返回-1成功返回0int listen(int s, int backlog);
6、接受连接上的客户端 struct sockaddr_in client_addr;socklen_t len sizeof(client_addr);int connet_fd accept(listen_fd, (struct sockaddr *)&client_addr, &len);// 这里需要注意同样涉及类型强制转换// 最后一个参数必须传指针// 返回一个 用于处理客户端请求的 套接字描述符
7、收发数据完成业务逻辑 // 此处开启一个新的线程来处理客户端的请求#include <pthread.h>pthread_t pid;pthread_create(&pid, NULL, deal_request, (void*)(long)client_fd);/** 处理客户端请求的逻辑* 读取数据成功先输出再直接发送过去* 读取数据失败关闭套接字*/void *deal_request(void* arg){ // 处理流程 return (void*)0;}
二、收发数据函数简介 1、发送数据 send ssize_t send(int sock_fd, const void *buf, size_t len, int flags);/** 参数* sock_fd发送给对方的网络套接字* buf待发送的数据的起始地址* len要发送的数据大小发送最多不超过len大小的字节* flags用于控制发送行为默认传0*/// 返回值是ssize_t表示实际发送成功字节数。返回-1表示出错
write ssize_t write(int sock_fd, const void *buf, size_t count);/** 参数* sock_fd发送给对方的网络套接字* buf待发送的数据的起始地址* count最大写入字节数*/
补充 套接字为阻塞模式时如果发送缓冲区无法容纳发送的数据程序会阻塞在send和write方法。
send、write函数向套接字发送数据时函数调用后不代表数据已经发送出去。网络协议栈有一个发送缓冲区先将数据拷贝到发送缓冲区然后网络协议栈将发送缓冲数据通过网卡驱动转为电信号给发送出去。
阻塞模式下发送缓冲区空间不够程序阻塞在send、write函数直到发送缓冲区数据发送出去腾出空间将剩下数据再拷贝到腾出的空间直接到数据全部拷贝进发送缓冲区函数返回。
2、接收数据 recvssize_t recv(int sock_fd, void *buf, size_t len, int flags);/** 参数* sock_fd从哪个套接字接收数据* buf接收到的数据保存到以buf为起始的地址* len本次最多接收多少字节的数据* flags控制套接字接收行为默认传0*//** 返回值* -1出错可以在errno取到对应的错误信息* 大于0实际读取到的字节数* 0(EOF)对端没有更多数据发送了可能对端已经把连接关闭*/
read ssize_t read (int sock_fd, void *buf, size_t count);/** 参数* sock_fd从哪个套接字接收数据* buf接收到的数据保存到以buf为起始的地址* count本次最多接收多少字节的数据*//** 返回值* -1出错可以在errno取到对应的错误信息* 大于0实际读取到的字节数* 0(EOF)对端没有更多数据发送了可能对端已经把连接关闭*/
补充 套接字阻塞模式下如果调用read和recv函数时套接字没有数据可读程序会阻塞在read或者recv函数直到有数据可读。
调用read、recv函数时从接收缓冲区读数据。所以不能保证每次调用read、recv函数时一定能读出所有数据。因为数据可能还在对端发送缓冲区也可能还在各个中间设备(路由器、交换机、电信主干网)。
接收缓冲区中有个读指针和写指针当调用read和recv函数时读指针会往后移下次读取就从新的指针处开始读读指针移动的长度就是read和recv函数返回的实际读取到的字节长度。
三、客户端与服务端通信示例 1、server.c (linux环境中)如果使用命令编译为可执行文件为gcc -o server server.c -lpthread
运行时执行命令./server
#include <stdio.h>#include <sys/types.h> #include <sys/socket.h>#include <netinet/in.h>#include <string.h>#include <pthread.h>#include <arpa/inet.h>/** 功能 线程函数处理客户端的请求* 这里打印接收到的信息打印后再返回给客户端*/void *deal_request(void *arg) {int fd (int)(long)arg;// linux中指针为long型char buffer[1024];while (1) {memset(buffer, 0, sizeof(buffer));int res recv(fd, buffer, sizeof(buffer), 0);if (res 0) {// 客户端退出连接close(fd);printf(客户端[%d]退出\n, fd);break;} else {printf(接收到客户端[%d]的数据是: %s\n, fd, buffer);send(fd, buffer, sizeof(buffer), 0);}}return (void*)0;}int main(void) {// 1、创建套接字int listen_fd socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt 1;unsigned int len sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口unsigned short port 5555;// 设置端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr htonl(INADDR_ANY);// 本机任何IP皆可// server_addr.sin_addr.s_addr inet_addr(192.168.237.10);// 指定IPserver_addr.sin_port htons(port);// 4、将套接字和IP、端口绑定int ret;ret bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));if (ret -1) {printf(bind failed\n);close(listen_fd);return -1;}// 5、开始监听ret listen(listen_fd, 5);if (ret -1) {printf(listen failed\n);close(listen_fd);return -1;}// 服务器不间断的接受客户端请求while (1) {// 6、接受连接上的客户端struct sockaddr_in client_addr;socklen_t len sizeof(client_addr);int connet_fd accept(listen_fd, (struct sockaddr *)&client_addr, &len);if (connet_fd -1) {printf(accept failed\n);close(listen_fd);return -1;}printf(客户端[%d]连接上\n, connet_fd);// 7、创建一个线程来处理当前客户端的请求pthread_t pid;pthread_create(&pid, NULL, deal_request, (void*)(long)connet_fd);}return 0;}
2、client.c (linux环境中) 对于客户端如果想不写代码可以使用NetAssist网络调试助手。如下图设置好网络协议、服务端IP和端口点击连接即可通信。在这之前确保服务端开放了设定的端口号或者直接关闭防火墙。
关闭防火墙我一般写成.sh文件如下:
#!/bin/bashsystemctl stop firewalld.service
如果也想通过代码进行通信client.c代码则如下
#include <stdio.h>#include <sys/types.h> #include <sys/socket.h>#include <netinet/in.h>#include <string.h>#include <pthread.h>#include <arpa/inet.h>int main(void) {// 1、创建套接字int client_fd socket(AF_INET, SOCK_STREAM, 0);// 2、设置服务端的IP和端口unsigned short port 5555;// 指定服务端的通信端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr inet_addr(192.168.237.10);// 指定服务端的IPserver_addr.sin_port htons(port);// 3、向服务端发起连接int ret;ret connect(client_fd,(struct sockaddr *)&servaddr,sizeof(servaddr)); if (ret -1){ printf(connect failed\n);close(sockfd);return -1; }// 4、与服务端进行通讯char buffer[1024];for(int i 0;i < 3;i ) { // 此处假设与将与服务端进行三次通讯 int iret; memset(buffer,0,sizeof(buffer)); sprintf(buffer,服务端你好这是第%d次通信。,i 1); // 生成报文内容 // 向服务端发送请求报文 iret send(client_fd, buffer, strlen(buffer), 0); if (iret -1){ printf(send failed\n); close(client_fd); return -1; } memset(buffer, 0, sizeof(buffer)); // 接收服务端响应报文如果服务端没有发送响应报文recv()函数将阻塞等待ret recv(client_fd, buffer, sizeof(buffer), 0); if (ret 0) {printf(服务端已经关闭);close(client_fd);return -1; } printf(接收到服务端信息%s\n, buffer); sleep(1);// 休眠1秒 } // 5、正常关闭socket释放资源 close(sockfd); return 0;}
3、客户端服务端代码流程对比 四、一些注意点 1、分包粘包问题 分包例如对方发送helloworld我方收到hello和world。
粘包例如对方发送hello和world我方收到helloworld。
解决办法TCP协议中数据以字节流方式传输被发送的数据可能不是一次性发完可能是被拆成很多个小段一段段发出去。有个重要前提就是TCP协议可以保证数据的顺序性。因此可以采用报文长度 报文内容的方法。也可以使用特殊分隔符如http协议采用\r\n。
2、阻塞与非阻塞阻塞阻塞I/O调用recv、read时程序切换到内核态。若I/O中套接字没有数据可读就阻塞。直到有数据可读内核将数据复制到用户态复制完再返回。最后recv、read函数再返回读取到的字节数。缺点是这种方式比较占CPU。
非阻塞非阻塞模式下没有读取到数据立即返回-1为了区别阻塞模式下返回-1可以查看错误码errno设置成了EAGAIN。缺点就是需要间隔一段时间就读一下数据涉及系统调用还是比较消耗资源。设置代码如下
// 头文件#include <unistd.h>#include <fcntl.h>fcntl(fd, F_SETFL, O_NONBLOCK); // fd为想要设置的套接字描述符
五、几种IO复用模型 1、select 原理 select方法告诉内核程序自己关心哪些I/O描述符当内核程序发现有I/O准备好可写/可读/异常内核程序将数据复制到用户态并从select返回。用户拿着准备好的IO再调用recv就一定能拿到数据。
用法和参数int select( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);/** 参数* maxfdp1当前待监听描述符基数。若监听的描述符最大值是3则maxfdp1就为4因为描述符从0开始* fd_set通常有读、写、异常三种情况readset、writeset、exceptset分别对应I/O的读、写和异常。 表示当前关心readset里描述符是否可读writeset里描述符是否可写exceptset中描述符是否有异常。* timeout struct timeval { long tv_sec; // 秒 long tv_usec; // 微秒 }- 传NULL如果没有I/O可以处理一直等待- 设置成对应的秒或微秒等待相应时间后若没有I/O可以处理就返回- 将tv_sec和tv_usec都设置成0表示不用等待立即返回。*//** select返回值* -1表示出错* 0表示超时* 大于0表示可操作的I/O数量。*/
使用流程 服务端 // 头文件#include <sys/select.h>// 1、创建监听的fd集合并初始化fd_set readfds; // 大小16字节1024位FD_ZERO(&readfds); // 每一位置0// 2、把listen的socket加入集合FD_SET(listensock, &readfds); // 起初还未有客户端加进来// 3、while循环中调用selectfd_set tmpfds readfds;// 复制一份fd集合因为系统在判断时会更改送进去的集合参数int infds select(maxfd 1, &tmpfds, NULL, NULL, 0);// 4、当select返回值大于0// 用FD_ISSET判断每个socket是否有事件FD_ISSET(eventfd, &tmpfds)/*对于新连接进来的客户端加入到事件集合FD_SET(clientsock, &readfds);*//*对于断开的客户端将其清除FD_CLR(eventfd, &readfds);并将套接字关闭close(eventfd); */
触发方式 采用水平触发如果报告fd事件没有被处理或数据没有被全部读取下次select时会再次报告该fd事件。
缺点1、select支持文件描述符数量太小默认1024
2、每次调整select都需要把fdset从用户态拷贝到内核
3、在线的大量客户端同时有事件发生的可能性小但还是需要遍历fdset因此随着监视的描述符数量增长效率也会线性下降。
完整的服务端程序server_select.c该程序中只监听了可读的事件并在liunx中运行编译命令gcc -o server server_select.c运行命令./server 5555。代码如下
#include <stdio.h>#include <sys/types.h>#include <sys/socket.h>#include <sys/select.h>#include <netinet/in.h>#include <string.h>int main(int argc, char *args[]) {if (argc ! 1 1) { // 需要传入参数端口printf(please give port!\n);return 0;}// 1、创建套接字int listen_fd socket(AF_INET, SOCK_STREAM, 0);// 2、设置端口复用int opt 1;unsigned int len sizeof(opt);setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, len);// 3、设置服务端的IP和端口struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family AF_INET;server_addr.sin_addr.s_addr htonl(INADDR_ANY);server_addr.sin_port htons(atoi(args[1]));// 4、将套接字和IP、端口绑定bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));// 5、开始监听listen(listen_fd, 5);// 6、初始化fd集和fd_set rfd;FD_ZERO(&rfd);FD_SET(listen_fd, &rfd);int maxfd listen_fd;while(1) {fd_set tmp_fd rfd;// 7、调用selectint num select(maxfd 1, &tmp_fd, NULL, NULL, 0);if (num -1) {printf(error select\n);close(listen_fd);break;} if (num 0) {// 没有事件继续continue;}// 如果listen_fd有事件if (FD_ISSET(listen_fd, &tmp_fd)) {struct sockaddr_in client_addr;socklen_t len sizeof(client_addr);// 8、接受连接上的客户端int client_fd accept(listen_fd, (struct sockaddr *)&client_addr, &len);if (client_fd -1) {printf(accept error\n);} else {printf(client %d was connected!\n, client_fd);FD_SET(client_fd, &rfd);if (client_fd > maxfd) maxfd client_fd;// 如果新的fd大于maxfd则替换}}// 检查后面的fd是否有事件int fd listen_fd 1;for (;fd < maxfd;fd ) {if (FD_ISSET(fd, &tmp_fd) 0) { // 无事件continue;} else { // 9、处理已经连接上的客户端的请求char buffer[1024];memset(buffer, 0, sizeof(buffer));int res recv(fd, buffer, sizeof(buffer), 0);if (res 0) { // 对方断开连接close(fd);printf(client [%d] disconnected !\n, fd);FD_CLR(fd, &rfd);// 从集和中清除} else {printf(recv data from [%d] is: %s\n, fd, buffer);send(fd, buffer, sizeof(buffer), 0);}}}}return 0;}
2、poll 原理 与select本质上没有差别管理多个描述符也进行轮询根据描述符状态进行处理但是poll没有最大文件描述符数量的限制。
用法和参数int poll( struct pollfd *fdarr, unsigned long nfds, int timout);/** 参数* fdarr要监听的I/O描述符事件集合其结构如下 struct pollfd { int fd; // 描述符 short events; // 监听描述符发生的事件 short revents; // 已经发生的事件 };* nfds要监听的套接字数量* timeout超时时间一般有三种传值的方式- -1表示在有可用描述符之前一直等待- 0表示不管有没有可用描述符都立即返回- 大于0表示超过对应毫秒即使没有事件发生也会立即返回*//** poll返回值* -1表示出错* 0表示超时* 大于0表示可操作的I/O数量。*/
使用流程 服务端 // 1、声明struct pollfd类型数组fdsint maxfd 2047; // linux中超过了2047有限制需要设置内核参数struct pollfd fds[maxfd 1]; // 2、初始化fds所有位置为-1表示忽略该元素poll在查找事件时就不会遍历这个元素for (int 0; i < maxfd; i) fds[i].fd -1;// 3、将服务端套接字放到fds第一个位置并设置监听POLLRDNORM事件fds[0].fd listensock;fds[0].events POLLIN; // 读事件// 4、循环里调用poll函数int infds poll(fds, maxfd 1, 10); // 超时时间为10毫秒/** 5、当poll返回值大于0* 先判断fds[i].fd是否为-1若不为再通过fds[eventfd].revents&POLLIN* 判断某个描述符是否有读事件*//** 如果是listensock* 接受新的客户端连接将新套接字* 找个-1的空位置存放起来并监听POLLRDNORM事件*//** 如果是原先存在的客户端事件则做相应的业务处理* 读取数据成功继续相应的业务处理操作* 读取失败将该位置设置为-1fds[i].fd -1;* 且关闭套接字close(fds[i].fd)*/
与select异同 相同点poll和select都会遍历所有描述符在连接数非常大时有性能问题而epoll就很好的解决该个问题。
不同点
1、select采用fd_set和bitmap而poll采用数组
2、在声明pollfd结构数据的时候可以自行指定大小linux超过2047需要设置内核参数
3、select会修改fd_set因此需要复制一份。poll不会修改pollfd它通过pollfd的events指定要监听的事件再通过revents保存已发生事件用于在poll返回时判断都有哪些事件发生。poll用两个short整型来保存监听事件和已经发生的事件。意味着调用poll之前不需要将监听的事件复制一份。I/O设置监听事件使用events判断是否有事件发生使用revents。
缺点与select类似poll文件描述符数组被整体复制于用户态和内核态的地址空间之间不论这些文件描述符是否有事件开销随着文件描述符数量增加而线性增大。poll返回后也需要历遍整个描述符数组才能得到有事件的描述符。
3、epoll 原理epoll原理比select、poll复杂得多后面单独用一篇文章介绍。
用法参数及使用流程// 1、创建epoll实例int epoll_create(int size);int epoll_create1(int flags);// 参数一般传0即可// 返回值大于0表示epoll实例-1表示出错// 2、注册要监听的fd和事件int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event);/** 参数* epfd使用poll_create创建出的epoll实例* op表示增、删、改分别对应: EPOLL_CTL_ADD向epoll实例注册文件描述符对应的事件 EPOLL_CTL_DEL删除epoll实例中文件描述符对应的事件 EPOLL_CTL_MOD修改epoll实例中文件描述符对应的事件。* fd要注册事件的描述符这里指网络套接字。* event是一个结构体如下 struct epoll_event { uint32_t events; // epoll事件 epoll_data_t data; }; - typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; 对于events一般设置如下 这里的事件与poll的基本一样 下面是在使用epoll的时候常用的事件类型 EPOLLIN表示描述符可读 EPOLLOUT表示描述符可写 EPOLLRDHUP表示描述符一端已经关闭或者半关闭 EPOLLHUP表示对应描述符被挂起 EPOLLET边缘触发模式edge-triggered不设置默认使用*//** epoll_ctl返回值* 0表示成功* -1表示出错*/// 3、等待事件发生int epoll_wait( int epfd, struct epoll_event *events, int maxevents, int timeout);/** 参数* epfd使用poll_create创建出的epoll实例* events要处理的I/O事件是个数组大小是epoll_wait的返回值每一个元素是一个待处理的I/O事件。* maxeventsepoll_wait可以返回的最大事件* timeout超时时间和select基本是一致的。 - 如果设置-1表示不超时 - 设置0表示立即返回*//** epoll_wait返回值 大于0表示事件个数 0表示超时 -1表示出错。*/
与poll区别 1、epoll需要使用poll_create创建一个实例后续所的操作都基于这个实例。
2、epoll不再是将fd设置成-1来表示忽略当前描述而是关心哪个就设置哪个使用epoll_ctl函数。
// 例如struct epoll_event event;event.data.fd sock_fd;event.events EPOLLIN | EPOLLET;epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &event)
3、events返回所有实际产生事件集合大小就是epoll_wait返回值。所以epoll_wait返回就可以确定从0到read_num所有位置都是有事件发生。而poll每次都从0遍历到最大描述字。这中间有很多没有事件发生的描述符。这种实现绕不开它背后的数据结构红黑树。
触发方式边沿触发只在第一次有数据可读的情况下通知一次。后面的处理就完全靠自己了很显然这种触发方式能够明显减少触发次数从而减轻内核的压力这在一些大数据量的传输场景下非常有用。
水平|条件触发epoll默认每次有数据可读时都会触发事件某些情况下会造成内核频发触发事件。
六、几种IO模型对比