C网络编程API(三)

一 socket函数底层调用过程

  1. 创建一个 socket 结构体(给OS用的),包括分配结构体空间(kmem_cache_allockmalloc),初始化结构体,再分配 sock 结构体(tcp_sock结构体)+初始化sock结构体(tcp_sock结构体)。
  2. 绑定linux文件系统的文件结构体,即建立 socket 结构体和(file结构体+fd)的联系,
在这里插入图片描述
在这里插入图片描述
  socket 系统调用的三个参数是一层一层递进向下的,首先用 domain 指明协议簇,然后通过第二个参数 type 指明该协议簇下面的协议类型,最后通过第三个参数 protocol 指明这种类型的具体协议。
  总结:用户通过得到的文件描述符 sock_fd 可以找到对于的 struct file 结构,而 struct file 结构有 struct socket 的引用,后者则包含了 struct sock 的引用。 在 struct socket 中有一套面向 socket layer 中的接口 struct proto_ops (inet_stream_ops)而在 struct sock 中有具体的协议的接口 struct proto (tcp_prot)

二 bind函数

  调用过程1. 通过fd找到file结构体,再找到socket结构体,再找到tcp_sock2. 把server_address参数拷贝到内核。3. 绑定:(1)检查是否被占用,(2)设置地址和端口
在这里插入图片描述
  端口的绑定:内核是通过一张哈希表(inet_hashinfo)知道端口是否被占用的,tcp socket是通过net/ipv4/tcp_ipv4.c 中的全局变量 tcp_hashinfo
inet_hashinfo 里面并不是只包涵一个哈希表,而是三个哈希表的集合。分别是:1. ehash - 用来索引 TCP_ESTABLISHED <= sk->sk_state < TCP_CLOSE 的 sock2. bhash - 用来索引绑定本地地址的 sock3. listening_hash - 用来索引 sk->sk_state 为 TCP_LISTEN 状态的 sock
在这里插入图片描述
  端口的选择:如果指定了端口,就会在哈希表中查找是否已经被占用以及是否可以重用,然后选择添加到哈希表中,如果没有指定端口的话,就会直接找到inet_bin_bucket结点进行判断是否能插入sock结构体。
  哈希表:内核设置了的inet_hashinfo其实是三个哈希表,

  1. ehash - 用来索引 TCP_ESTABLISHED <= sk->sk_state < TCP_CLOSE 的 sock
  2. bhash - 用来索引绑定本地地址的 sock
  3. listening_hash - 用来索引 sk->sk_state 为 TCP_LISTEN 状态的 sock

三 listen API底层实现

在这里插入图片描述
  客户端发送SYN同步请求报文,进入SYN_SEND状态
  ——>服务器端接受到客户端的套接字的SYN请求同步报文SYN_RCVD状态,并将套接字放置内核底层的SYN队列中(未完成TCP三次握手的队列),并向客户端发送SYN,ACK报文
  ——>客户端接受到服务器端的SYN,ACK报文,并再向服务器端发送ACK报文进入ESTABLISHED状态
  ——>服务器收到客户端的ACK确认报文,1.从队列中取出套接字,2.插入ACCEPT队列(已完成TCP三次握手的队列)中,套接字进入ESTABLISHED状态。等待服务器应用程序通过accept()API从ACCEPT队列中取走该连接套接字。
  注意,已完成三次握手的ACCEPT队列是动态的链表可以动态增长的,listen的backlog参数是供给未完成三次握手的队列的,是静态数组(零长数组)。队列的长度是由三个元素决定的。它的最大值由下面三个中的最小值决定:参数 backlog , sysctl_max_syn_backlog 变量, sock_net(sock->sk)->core.sysctl_somaxconn
  TCP/IP协议栈接受到一个新的客户端数据包时,判断如果是请求连接的数据包,则传给监听着连接端口的listenfd,如果是已经建立连接的客户端数据包,则将数据放置接受缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用socketfd_new套接字通过recv或者read函数到缓冲区里面去取指定的数据。
  数据包如何找到相对应的socket,这个方法在linux kernel代码里也是有体现的。

static inline struct sock *__inet_lookup(struct net *net,
					 struct inet_hashinfo *hashinfo,
					 const __be32 saddr, const __be16 sport,
					 const __be32 daddr, const __be16 dport,
					 const int dif)
{
	u16 hnum = ntohs(dport);
	/* 先尝试查找处于连接成功的socket */
	struct sock *sk = __inet_lookup_established(net, hashinfo, 
				saddr, sport, daddr, hnum, dif);
	/* 如果没有找到连接成功的socket,那么就去处于listen状态的socket查找 */
	return sk ? : __inet_lookup_listener(net, hashinfo, daddr, hnum, dif);
}

  3. 检测端口重用?
  4. 加入listening_hash这个处于TCP_LISTEN状态的sock哈希表。(ehash(established)、bhash(bind)、
listening_hash)。
  两个队列(如下图)满了后,新的请求到达了后的处理过程:
在这里插入图片描述
  若SYN队列满,则会直接丢弃请求;客户端没有收到ACK+SYN就会重发SYN
  应用程序没有调用accept()从ACCEPT队列中获取已完成三次握手的套接字会导致ACCEPT队列满了, 这个时候,如果SYN队列中有完成俩次握手的套接字收到了ACK,那么这会儿已经没有ACCEPT队列位置了,如果内核中设置了
tcp_abort_on_overflow标志为1的话,会向客户端发送RST包,表示要求客户端重新连接,如果没有设置的话,就简单
的丢弃ACK,这样服务器就是没有收到这个ACK,如果一段时间服务器还是没有重新收到ACK的话,就会向客户端重新发
送ACK+SYN,这会儿客户端又收到了ACK+SYN就会重新发送ACK了。
  服务端SYN超时
当客户端给服务端发送SYN报文时,如果服务端没有返回SYN+ACK报文,那么客户端会重发SYN报文给服务端,重发的
次数由参数tcp_syn_retries参数设置,该值默认是5,超过5次服务端还是不返回SYN+ACK报文,那么本次连接失败。
服务端没有返回SYN+ACK主要有两种情况,一种是由于网络问题SYN包丢失;另一种是服务端SYN队列满,导致SYN包
被丢弃。
客户端ACK超时
如果服务端接到了客户端发的SYN并回发SYN+ACK后,客户端掉线了,这时,服务端没有收到客户端回来的ACK,那
么,这个连接处于一个中间状态,既没成功也没失败。于是,服务端端如果在一定时间内没有收到客户端端的ACK,那么
服务端端会重发SYN+ACK。在Linux下,默认重试次数为5次,重发的间隔时间从1s开始每次都翻番(指数退避),5次
的重发的时间间隔分别1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要
1s+2s+4s+8s+16s+32s = 2^6-1 = 63s,TCP才会把断开这个连接。、
4. SYN Flood
这时一种恶意攻击。客户端给服务器发一个SYN后就下线,这样服务器需要默认等待63s才会断开连接,这样攻击者就可
以把服务器的SYN连接队列耗尽,让正常的连接请求不能处理。为了应对SYN Flood攻击,Linux实现了一种称为SYN
cookie的机制,通过net.ipv4.tcp_syncookies来设置。当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时
间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会
把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使不在SYN队列中),还有一种方式就是缩短SYN
Timeout时间。

四 accept函数

  每个 sock 在底层都有一个wait queue等待队列,里面挂接着进程,当相应的 sock 从ACCEPT队列拿出来时候,会唤醒相应的等待队列中的进程去处理。
在这里插入图片描述

五 recv

原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
说明:读取 sockfd 上的数据,buf和 len 参数分別指定读缓冲区的位置和大小,flags 参数的含义见后文,通常设置为0 即可。recv 成功时返回实际读取到的数据的长度,它可能小于我们期望的长度 len。因此我们可能要多次调用recv,才能读取到完整的数据。
在这里插入图片描述
返回值:
0:这意味着通信对方已经关闭连接了。
-1:如果在读的过程中遇到了中断那么会返回-1,同时置errno为EINTR。recv 出错时返问-1,并设置 errno。在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
  在非阻塞模式下,无论操作是否完成都会立刻返回,需要通过其他方式来判断具体操作是否成功。(对于connect,accpet操作,通过select判断,对于recv,recvfrom,send,sendto通过返回值+错误码来判断)

EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
EBADF:sock不是有效的描述词
ECONNREFUSE:远程主机阻绝网络连接
EFAULT:内存空间访问出错
EINTR:操作被信号中断
EINVAL:参数无效
ENOMEM:内存不足
ENOTCONN:与面向连接关联的套接字尚未被连接上
ENOTSOCK:sock索引的不是套接字

六 getsockopt和setsockopt

  下面两个系统调用则是专门用来读取和设置 socket 文件描述符属性的方法:

#include <sys/socket.h>
int getsockopt(int sockfd, int level, int option_name, void
				*option_value, socklen_t * restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void *
				option_value, socklen_t option_len);

  sockfd 参数指定被操作的目标 socket。level 参数指定要操作哪个协议的选项 (即属性),比如IPv4、IPv6、TCP 等。option_name 参数则指定选项的名字。
  以下是 socket 通信中几个比较常用的 socket 选项。 option_value 和 option_len 参数分別是被操作选项的值和长度,不同的选项具有不同类型的值。
在这里插入图片描述

七 非阻塞socket

  1、socket系统调用的第 2 个参数传递SOCK_NONBLOCK标志;
  2、fcntl系统调用的F_SETFL命令。

flags = fcntl(sockfd, F_GETFL, 0);//获取文件的flags值。
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);   //设置成非阻塞模式;
flags  = fcntl(sockfd,F_GETFL,0);
fcntl(sockfd,F_SETFL,flags&~O_NONBLOCK);    //设置成阻塞模式;

//并在接收和发送数据时:将recv, send 函数的最后有一个flag 参数设置成MSG_DONTWAIT
recv(sockfd, buff, buff_size,MSG_DONTWAIT);     //非阻塞模式的消息发送
send(scokfd, buff, buff_size, MSG_DONTWAIT);   //非阻塞模式的消息接受

八 sendfile

  如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序的角度来阐述这个问题,因此有意忽略了复杂的内核实现。
  什么是”零拷贝”
  为了更好的理解问题的解决法,我们首先需要理解问题本身。首先我们以一个网络服务守护进程为例,考虑它在将存储在文件中的信息通过网络传送给客户这样的简单过程中,所涉及的操作。下面是其中的部分简单代阿:read(file, tmp_buf, len);write(socket, tmp_buf, len);
  看起来不能更简单了。你也许认为执行这两个系统调用并未产生多少开销。实际上,这简直错的一塌糊涂。在执行这两个系统调用的过程中,目标数据至少被复制了4次,同时发生了同样多次数的用户/内核空间的切换(实际上该过程远比此处描述的要复杂,但是我希望以简单的方式描述之,以更好的理解本文的主题)。
  为了更好的理解这两句代码所涉及的操作,请看图1。图的上半部展示了上下文切换,而下半部展示了复制操作。
在这里插入图片描述
  步骤一:系统调用read导致了从用户空间到内核空间的上下文切换。DMA模块从磁盘中读取文件内容,并将其存储在内核空间的缓冲区内,完成了第1次复制。
  步骤二:数据从内核空间缓冲区复制到用户空间缓冲区,之后系统调用read返回,这导致了从内核空间向用户空间的上下文切换。此时,需要的数据已存放在指定的用户空间缓冲区内(参数tmp_buf),程序可以继续下面的操作。
  步骤三:系统调用write导致从用户空间到内核空间的上下文切换。数据从用户空间缓冲区被再次复制到内核空间缓冲区,完成了第3次复制。不过,这次数据存放在内核空间中与使用的socket相关的特定缓冲区中,而不是步骤一中的缓冲区。
  步骤四:系统调用返回,导致了第4次上下文切换。第4次复制在DMA模块将数据从内核空间缓冲区传递至协议引擎的时候发生,这与我们的代码的执行是独立且异步发生的。你可能会疑惑:“为何要说是独立、异步?难道不是在write系统调用返回前数据已经被传送了?write系统调用的返回,并不意味着传输成功——它甚至无法保证传输的开始。调用的返回,只是表明以太网驱动程序在其传输队列中有空位,并已经接受我们的数据用于传输。可能有众多的数据排在我们的数据之前。除非驱动程序或硬件采用优先级队列的方法,各组数据是依照FIFO的次序被传输的(图1中叉状的DMA copy表明这最后一次复制可以被延后)。
  正如你所看到的,上面的过程中存在很多的数据冗余。某些冗余可以被消除,以减少开销、提高性能。作为一名驱动程序开发人员,我的工作围绕着拥有先进特性的硬件展开。某些硬件支持完全绕开内存,将数据直接传送给其他设备的特性。
这一特性消除了系统内存中的数据副本,因此是一种很好的选择,但并不是所有的硬件都支持。此外,来自于硬盘的数据
必须重新打包(地址连续)才能用于网络传输,这也引入了某些复杂性。为了减少开销,我们可以从消除内核缓冲区与用户
缓冲区之间的复制入手。
  消除复制的一种方法是将read系统调用,改为mmap系统调用,例如:tmp_buf = mmap(file, len);write(socket, tmp_buf, len);
  为了更好的理解这其中设计的操作,请看图2。上下文切换部分与图1保持一致。
在这里插入图片描述
  步骤一:mmap系统调用导致文件的内容通过DMA模块被复制到内核缓冲区中,该缓冲区之后与用户进程共享,这样就内核缓冲区与用户缓冲区之间的复制就不会发生。
  步骤二:write系统调用导致内核将数据从内核缓冲区复制到与socket相关联的内核缓冲区中。
  步骤三:DMA模块将数据由socket的缓冲区传递给协议引擎时,第3次复制发生。
  通过调用mmap而不是read,我们已经将内核需要执行的复制操作减半。当有大量数据要进行传输是,这将有相当良好的效果。然而,性能的改进需要付出代价的;是用mmap与write这种组合方法,存在着一些隐藏的陷阱。例如,考虑一下在内存中对文件进行映射后调用write,与此同时另外一个进程将同一文件截断的情形。此时write系统调用会被进程接收到的SIGBUS信号中断,因为当前进程访问了非法内存地址。对SIGBUS信号的默认处理是杀死当前进程并生成dump core文件——而这对于网络服务器程序而言不是最期望的操作。
  有两种方式可用于解决该问题:
  第一种方式是为SIGBUS信号设置信号处理程序,并在处理程序中简单的执行return语句。在这样处理方式下,write系统调用返回被信号中断前已写的字节数,并将errno全局变量设置为成功。必须指出,这并不是个好的解决方式——治标不治本。由于收到SIGBUS信号意味着进程发生了严重错误,我不鼓励采取这种解决方式。
  第二种方式应用了文件租借(在Microsoft Windows系统中被称为“机会锁”)。这才是解劝前面问题的正确方式。通过对文件描述符执行租借,可以同内核就某个特定文件达成租约。从内核可以获得读/写租约。当另外一个进程试图将你正在传输的文件截断时,内核会向你的进程发送实时信号——RT_SIGNAL_LEASE。该信号通知你的进程,内核即将终止在该
文件上你曾获得的租约。这样,在write调用访问非法内存地址、并被随后接收到的SIGBUS信号杀死之前,write系统调用就被RT_SIGNAL_LEASE信号中断了。write的返回值是在被中断前已写的字节数,全局变量errno设置为成功。下面是一段展示如何从内核获得租约的示例代码。

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
} /
* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}

  sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。使用方法如下:sendfile(socket, file, len);
  为了更好的理解所涉及的操作,请看图3.Replacing Read and Write with Sendfile
在这里插入图片描述
  步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到某个内核缓冲区,之后再被复制到与socket相关联的缓冲区内。
  步骤二:当DMA模块将位于socket相关联缓冲区中的数据传递给协议引擎时,执行第3次复制。你可能会在想,我们在调用sendfile发送数据的期间,如果另外一个进程将文件截断的话,会发生什么事情?如果进程没有为SIGBUS注册任何信号处理函数的话,sendfile系统调用返回被信号中断前已发送的字节数,并将全局变量errno置为成功。
  然而,如果在调用sendfile之前,从内核获得了文件租约,那么类似的,在sendfile调用返回前会收到RT_SIGNAL_LEASE。
  到此为止,我们已经能够避免内核进行多次复制,然而我们还存在一分多余的副本。这份副本也可以消除吗?当然,在硬件提供的一些帮助下是可以的。为了消除内核产生的素有数据冗余,需要网络适配器支持聚合操作特性。该特性意味着待发送的数据不要求存放在地址连续的内存空间中;相反,可以是分散在各个内存位置。在内核版本2.4中,socket缓冲区描述符结构发生了改动,以适应聚合操作的要求——这就是Linux中所谓的"零拷贝“。这种方式不仅减少了多个上下文切换,而且消除了数据冗余。从用户层应用程序的角度来开,没有发生任何改动,所有代码仍然是类似下面的形式:sendfile(socket, file, len);
  为了更好的理解所涉及的操作,请看图4
在这里插入图片描述
  步骤一:sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中。
  步骤二:数据并未被复制到socket关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。
  由于数据实际上仍然由磁盘复制到内存,再由内存复制到发送设备,有人可能会声称这并不是真正的"零拷贝"。然而,从操作系统的角度来看,这就是"零拷贝",因为内核空间内不存在冗余数据。应用"零拷贝"特性,出了避免复制之外,还能获得其他性能优势,例如更少的上下文切换,更少的CPU cache污染以及没有CPU必要计算校验和。现在我们明白了什么是"零拷贝",让我们将理论付诸实践,编写一些代码。你可以从www.xalien.org/articles/source/sfl-src.tgz处下载完整的源码。执行"tar -zxvf sfl-src.tgz"将源码解压。运行make命令,编译源码,并创建随机数据文件data.bin
  从头文件开始介绍代码:

/* sfl.c sendfile example program
Dragan Stancevic <
header name function / variable
-------------------------------------------------*/
#include <stdio.h> /* printf, perror */
#include <fcntl.h> /* open */
#include <unistd.h> /* close */
#include <errno.h> /* errno */
#include <string.h> /* memset */
#include <sys/socket.h> /* socket */
#include <netinet/in.h> /* sockaddr_in */
#include <sys/sendfile.h> /* sendfile */
#include <arpa/inet.h> /* inet_addr */
#define BUFF_SIZE (10*1024) /* size of the tmp buffer */

  除了基本socket操作所需要的 <sys/socket.h> 和<netinet/in.h>头文件外,我们还需要包含sendfile系统调用的原型定义,这可以在<sys/sendfile.h>头文件中找到。
  服务器标志:

/* are we sending or receiving */
if(argv[1][0] == 's') is_server++;
/* open descriptors */
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);

  该程序既能以服务端/发送方,也能以客户端/接收方的身份运行。我们需要检查命令行参数中的一项,然后相应的设置is_server标志。程序中大开了一个地址族为PF_INET的流套接字;作为服务端运行时需要向客户发送数据,因此要打开某个数据文件。由于程序中是用sendfile系统调用来发送数据,因此不需要读取文件内容并存储在程序的缓冲区内。
  接下来是服务器地址:

/* clear the memory */
memset(&sa, 0, sizeof(struct sockaddr_in));
/* initialize structure */
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);

  将服务端地址结构清零后设置协议族、端口和IP地址。服务端的IP地址作为命令行参数传递给程序。端口号硬编码为1033,选择该端口是因为它在要求root权限的端口范围之上。
  下面是服务端的分支代码:

if(is_server){
int client; /* new client socket */
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,sizeof(sa)) < 0){
perror("bind");
exit(errno);
}

  作为服务端,需要为socket描述符分配一个地址,这是通过系统调用bind完成的,它将服务器地址(sa)分配给socket描述符(sd).

if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}

  由于使用流套接字,必须对内核声明接受外来连接请求的意愿,并设置连接队列的尺寸。此处将队列长度设为1,但是通常会将该值设的高一些,用于接受已建立的连接。在老版本的内核中,该队列被用于阻止SYN flood攻击。由于listen系统调用之改为设定已建立连接的数量,该特性已被listen调用遗弃。内核参数tcp_max_syn_backlog承担了保护系统不受SYN flood攻击的功能。

if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}

  accept系统调用从待处理的已连接队列中选取第一个连接请求,为之建立一个新的socket。accept调用的返回值是新建立连接的描述符;新的socket可以用于read、write和poll/select系统调用。

if((cnt = sendfile(client,fd,&off, BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
} p
rintf("Server sent %d bytes.\n", cnt);
close(client);

  在客户socket描述符上已经建立好连接,因此可以开始将数据传输至远端系统——这时通过调用sendfile系统调用来完成。该调用在Linux中的原型为如下形式:

extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset, size_t __count) __THROW;

  前两个参数为文件描述符,第三个参数表示sendfile开始传输数据的偏移量。第四个参数是打算传输的字节数。为了sendfile可以使用"零拷贝“特性,网卡需要支持聚合操作,此外还应具备校验和计算能力。如果你的NIC不具备这些特
性,仍可以是用sendfile来发送数据,区别是内核在传输前会将所有缓冲区的内容合并。
  移植性问题
  sendfile系统调用的问题之一,总体上来看,是缺少标准化的实现,这与open系统调用类些。sendfile在Linux、Solaris
或HP-UX中的实现有很大的不同。这给希望在网络传输代码中利用"零拷贝"的开发者带来了问题。
  这些实现差异中的一点在于Linux提供的sendfile,是定义为用于两个文件描述符之间和文件到socket之间的传输接口。另一方面,HP-UX和Solaris中,sendfile只能用于文件到socket的传输。
  第二点差异,是Linux没有实现向量化传输。Solaris和HP-UX 中的sendfile系统调用包含额外的参数,用于消除为待传输数据添加头部的开销。
  展望
  Linux中“零拷贝”的实现还远未结束,并很可能在不久的未来发生变化。更多的功能将会被添加,例如,现在的sendfile不支持向量化传输,而诸如Samba和Apache这样的服务器不得不是用TCP_COKR标志来执行多个sendfile调用。该标志告知系统还有数据要在下一个sendfile调用中到达。TCP_CORK和TCP_NODELAY不兼容,后者在我们希望为数据添加头
部时使用。这也正是一个完美的例子,用于说明支持向量化的sendfile将在那些情况下,消除目前实现所强制产生的多个sendfile调用和延迟。
  当前sendfile一个相当令人不愉快的限制是它无法用户传输大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕见,不得不复制数据是十分令人失望的。由于这种情况下sendfile和mmap都是不可用的,在未来内核版本中提供
sendfile64,将会提供很大的帮助。
  结论
  尽管有一些缺点,"零拷贝"sendfile是一个很有用的特性。我希望读者认为本文提供了足够的信息以开始在程序中使用sendfile。如果你对这个主题有更深层次的兴趣,敬请期待我的第二篇文章——“Zero Copy II: Kernel Perspective”,在其中将更深一步的讲述"零拷贝"的内核内部实现。

九 socket编程中父子进程、兄弟进程的端口问题

  假如父进程监听A端口,那么在client端来连接,并fork子进程,那么子进程通过A端口与client交换数据呢。使用端口复用技术。
  socket句柄的问题,父子进程中的文件句柄是一样的,而且在子进程中对文件句柄进行操作会效果和在父进程的效果是一样的。我们确定的访问对象是由一个暂且称之为进程环境的东西规定的,socket句柄的进程环境,由子进程的端口以及对应的连接的client的端口确定的。

十 多个子进程对同一个端口进行操作,包如何向上提交问题

在这里插入图片描述
  通过本例应注意,TCP无法仅仅通过目的端口来分离外来的分节。它必须查看套接口对的所有四个元素才能确定由哪个端口到达的分节。
  socket
  sys_socket:创建一个插口,接口为:int socket(int domain, int type, int protocol)
  这里的三个参数即为前面所述插口的三个要素。不过,通常第三个参数protocol为0,因为一般来说前两个参数确定以后,具体的规程就确定了,所以用0表示默认由系统根据前两个参数确定的规程,只有在比较特殊的应用中才需要指定具体的规程。返回的文件描述符与一个代表插口的数据结构相连。
  每个msghdr数据结构都代表着一个报文。在msghdr中,msg_name为对方的插口地址(或者也可以称做插口名)。而msg_iov则指向一个结构数组,该数组中的每一个元素都是一块数据,这样一个报文的内容就可以分散在若干个互不相连的缓冲区中,而在逻辑上确连在一起,在网络环境下这样是有好处的。还有,msg_control和msg_controllen的作用是传递控制消息,在unix域中用来在进程间传递访问权限,还可以用来传递“打开文件描述体”。

十一 linux socket错误码及其获取

  在网络编程中很多情况都是在发送和接收数据时出现了socket上有异常导致操作无法完成,而返回值只能涉及到操作相关的字节数和是否错误,并不能反映完全的错误信息。所以需要获取socket上的具体错误信息。

1 strerror

  这个函数以及errno全局变量是最常用的获取Linux中错误信息的函数。因此使用起来相当顺手,而且这个函数也可以捕捉所有的Linux中的错误,因为其使用的错误号是全局变量。
  劣势也在于此:在获取这个错误时不能完全保证这个错误信息就是之前的,很由可能在获取该信息时错误号和错误信息已经再度更新修改了,会造成误判。在网络编程中,特别是在异步的网络操作时,检测到错误后,再去获取错误是又时间差的,容易被覆盖修改。

2 gai_strerror

  有很多socket相关的函数的错误号和错误信息是无法通过errno,strerror(errno)函数去获取的。其原因在于很多函数并没有将errno.h作为错误码。
  相关说明:if getaddrinfo fails, we can’t use perror or strerror to generate an error message. Instead, we need to call gai_strerrorto convert the error code returned into an error message.
  下面的 getnameinfo 也用 gai_strerror 收集错误信息。
  这也是不能用 perror or strerror 处理的原因,因为它没有用 errno(#include <errno.h>) 作为错误代码。所以使用gai_strerror的主要是为了统一OS的转换的getnameinfo 、getaddrinfo 之类的函数,需要尤其注意。
  缺点:其参数是getnameinfo 、getaddrinfo 之类的函数的执行后的返回值,所以只适用于特定范围。

3 getsockopt(第三个参数SO_ERROR)

  这个函数将获取fd上的错误信息。如果epoll、select、poll检测到fd上有异常,那么通过getsockopt的SO_ERROR来获取fd上的错误码无疑是最准确地。

  因此,综上所述,这些错误信息获取方式各有优缺点和适宜的场景,大家可以根据使用场景,合理的去调用。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值