这段代码来自于游双的《Linux高性能服务器编程》,在Ubuntu中对代码进行了实现,并在注释部分加上了我的个人解读。
//
#include
// 网络通讯的核心函数都在这
#include
//
#include
#include
//
#include
#include
#include
#include
#include
//
#include
//
#include
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和port
const 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都需要包含头文件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多提供了一个标识作为参数,这也正是它的优点之一:可以使用额外的控制标志来控制数据传输的行为。
虽然它们都将文件描述符作为第一个参数,但是实际上它们是有区别的:
这个我觉得是一个容易出错的点,但是想想好像也很好理解。
错误检测:这些函数的返回值都表示读取/发送的字节数,若是返回值为-1,则说明发生了错误,同时它们会设置error,因此我们可以以此来进行异常检测。
recv/send的最后一个参数用来控制数据传输的行为,在本文中,我们已经使用了MSG_OOB用于紧急数据传输,除此之外还有很多的常用的标识,例如:
具体的解释我们以后再说。
总的来说:recv()和send()函数更适用于处理网络连接和套接字流量控制,具有更多的控制选项,而read()和write()函数更通用。在此处我们使用了select模型,该模型需要将数据类型分类,我想,这也许就是作者使用recv而不是read的原因吧。