文章目录
- 前言
- 源代码
- 部分重点解读
- read/write与recv/send
- 在使用上的差异
前言
这段代码来自于游双的《Linux高性能服务器编程》,在Ubuntu中对代码进行了实现,并在注释部分加上了我的个人解读。
源代码
//
#include <sys/types.h>
// 网络通讯的核心函数都在这
#include <sys/socket.h>
//
#include <netinet/in.h>
#include <arpa/inet.h>
//
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
//
#include <fcntl.h>
//
#include <stdlib.h>int main(int argc, char* argv[]){if(argc < 2){// basename这个函数在string.h中// 这个函数会去除文件名中的目录部分,只留下真正的文件名printf("usage:%s ip_address port_number\n", basename(argv[0]));return 1;}// 设置ip和portconst char* ip = argv[1];// 这个atoi其实就是C++string中的stoi,其中的a是ASCII的缩写int port = atoi(argv[1]);// 使用ret来接收函数的返回值,以此来判断程序是否出错int ret = 0;sockaddr_in address;bzero(&address, sizeof(sockaddr_in));address.sin_family = AF_INET;/*因为我们输入的内容通常是点分十进制但是在网络传输的实际过程中,ip和port通常都需要在二进制的形式下进行处理此函数将ip转换为二进制后,将其设置为sockaddr_in.sin_addr这个函数在头文件arpa/inet.h中*/inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);// 创建监听套接字int listenfd = socket(PF_INET, SOCK_STREAM, 0);/*这是一个断言它判断条件是否满足若是满足,程序继续运行;若是不满足,程序终止运行,并输出错误信息*/assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));assert(ret != -1);// 第二个参数是指:能够同时处理的最大连接数ret = listen(listenfd, 5);assert(ret != -1);struct sockaddr_in client_address;/*用于表示表示套接字地址结构长度的数据类型。是一个无符号整数类型,在套接字编程中用于指定套接字地址结构的长度。socklen_t类型的变量则用于指示地址结构的大小,常用于与函数accept()、bind()、connect()等配合使用。*/socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);// 如果套接字创建失败if(connfd < 0){printf("errno is:%d", errno);close(listenfd);}/*有点不明觉厉*/char buf[1024];fd_set read_fds;fd_set exception_fds;FD_ZERO(&read_fds);FD_ZERO(&exception_fds);while(1){memset(buf, '\0', sizeof(buf));/*每次调用select前都要重新在read_fds和expection_fdfs中设置文件描述符connfd因为事件发生后,文件描述符集合将被内核修改将文件描述符:connfd的状态进行设置同时对其开启“读”和“异常”处理*/FD_SET(connfd, &read_fds);FD_SET(connfd, &exception_fds);/*这里就有个问题了,第一个参数它是什么?是:指定被监听文件描述符的总数而文件描述符是从0开始的,因此需要+1*/ret = select(connfd+1, &read_fds, NULL, &exception_fds, NULL);if(ret < 0){printf("selection failure");break;}/*对于可读事件,采用普通的recv函数读取数据*/if(FD_ISSET(connfd, &read_fds)){/*可以发现,大佬在使用recv的时候使用的是sizeof(buf)-1这是因为在C格式的字符串中,存在结尾标识符,即:\0而-1能够保证我们能够很完整得读取数据*/ret = recv(connfd, buf, sizeof(buf)-1, 0);if(ret < 0){break;}printf("get%d bytes of normal data:%s\n", ret, buf);}/*对于异常事件,采用才MSG_OOB标志的recv函数读取带外数据(这个地方看得不是太懂)*/else if(FD_ISSET(connfd, &exception_fds)){/*recv的最后一个参数就是指定传输的数据类型MSG_OOB就是带外数据的意思这个宏在send和recv中常常被使用*/ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);if(ret < 0){break;}printf("get#d bytes of oob data:%s\n", ret, buf);}}close(connfd);close(listenfd);return 0;
}
部分重点解读
在这篇文章之前,我已经写完我的Linux网络编程的day01与day02,可能会有细心的读者发现:在先前我对接收到的数据进行处理使用的是read和write,而在此处我使用的是recv和send,这是因为我两部分代码的出处不同,有的是来自的Github的开源教程,有的是来自《Linux高性能网络编程》一书。
实际上,这两个函数的差别不是很大。我们都知道:在Linux中,万事万物皆文件,网络编程的本质也是对文件进行操作。
read和write是使用的Linux系统提供的接口,而recv和send则是套接字提供的函数,这里就来说说它两的区别。
read/write与recv/send
在先前的代码中,我们使用read/write都需要包含头文件unistd.h,我们先来看看read/write的函数声明(Linux下的GCC编译器):
ssize_t read (int __fd, void *__buf, size_t __nbytes);
ssize_t write (int __fd, const void *__buf, size_t __n);
这个声明可以说是一看就懂,那我也就不过多赘述了。
接下来重点说说recv/send,先前说过,这两个函数由网络通信提供,因此就在头文件,sys/socket.h中:
ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
ssize_t send (int __fd, const void *__buf, size_t __n, int __flags);
可以发现:recv/send多提供了一个标识作为参数,这也正是它的优点之一:可以使用额外的控制标志来控制数据传输的行为。
在使用上的差异
虽然它们都将文件描述符作为第一个参数,但是实际上它们是有区别的:
- read/write的第一个参数是要处理的文件的文件描述符
- recv/send是将已连接的套接字描述符作为参数
这个我觉得是一个容易出错的点,但是想想好像也很好理解。
错误检测:这些函数的返回值都表示读取/发送的字节数,若是返回值为-1,则说明发生了错误,同时它们会设置error,因此我们可以以此来进行异常检测。
recv/send的最后一个参数用来控制数据传输的行为,在本文中,我们已经使用了MSG_OOB用于紧急数据传输,除此之外还有很多的常用的标识,例如:
- MSG_DONTWAIT:在非阻塞模式下进行数据传输。如果没有可用的数据或缓冲区已满,函数会立即返回,而不会阻塞等待。该标志可用于确保接收或发送操作不会阻塞应用程序。
- MSG_PEEK:从套接字缓冲区中查看数据,但不将其从缓冲区中移除。这样可以检查下一个数据报或消息的内容,而不会实际读取它。该标志可用于预览数据而不丢失它们,或者在处理选择器时检查可读或可写事件的条件。
- MSG_WAITALL:在接收数据时,要求函数等待,直到请求的数据完全接收到缓冲区中后再返回。如果没有足够的数据可用,则函数将阻塞等待直到所有请求的数据接收完毕。
具体的解释我们以后再说。
总的来说:recv()和send()函数更适用于处理网络连接和套接字流量控制,具有更多的控制选项,而read()和write()函数更通用。在此处我们使用了select模型,该模型需要将数据类型分类,我想,这也许就是作者使用recv而不是read的原因吧。