网络协议栈(5)sendto/send返回成功意味着什么
作者:互联网
一、有连接与无连接
上层编程的时候我们比较常见的就是UDP使用sendto发送,而TCP使用send发送,前者是无连接的,后者是面向连接的。或者通俗的说,TCP比UDP更靠谱一些。所谓面向连接就是说这个发送协议栈担任了雷锋侠的角色,就是默默地对链路进行了协调和维护,从而让这个链路看起来更加的稳定。例如,用户态的代码不需要分配报文的序列号,不需要对报文进行确认,不需要处理超时重传等工作,这些都有TCP协议层来代劳。由于UDP比较奔放一点,所以它不会处理这些比较需要耐心的工作。
那么这个连接体现在sendto/send的区别在哪里呢?比方说,如果sendto返回成功,我们可以想当然的认为这个发送可能事实上并没有到达对端,因为UDP本身就没有承诺是一定送到的。但是TCP就真的可以依赖吗?send返回成功之后是不是报文就一定能到达对方呢?如果不是,那么这个报文将至少到了哪里?
二、TCP/UDP测试
1、UDP测试
现在做一个简单的测试,用来测试UDP的sendto,让这个sendto向一个不存在的局域网地址发送(但是本机到目的IP的路由存在,例如,我的192.168.203.0网段,所以发送的目的为192.168.203.119)数据,看一下UDP的反应
[root@Harry sendto]# cat sendto.c
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc , char * argv[])
{
int sockfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
struct sockaddr_in sockrcv;
sockrcv.sin_family =AF_INET;
sockrcv.sin_port=htons(1234);
inet_pton(AF_INET,argv[1],&sockrcv.sin_addr.s_addr);
if ( 0!=connect(sockfd,(const struct sockaddr*)&sockrcv,sizeof(sockrcv)))
{
perror("connect\n");
exit(errno);
}
else
printf("connect OK\n");
char sendbuf[] = "Hello Nobody";
while(1)
{
if (sizeof(sendbuf) == send(sockfd,sendbuf,sizeof(sendbuf),0))
printf("send successful\n");
else
perror("Send failed\n");
//sleep();
}
}
[root@Harry sendto]# cat Makefile
default:
gcc sendto.c -o sendto.exe
[root@Harry sendto]# make
gcc sendto.c -o sendto.exe
[root@Harry sendto]# ping 192.168.203.119
PING 192.168.203.119 (192.168.203.119) 56(84) bytes of data.
From 192.168.203.155 icmp_seq=2 Destination Host Unreachable
From 192.168.203.155 icmp_seq=3 Destination Host Unreachable
From 192.168.203.155 icmp_seq=4 Destination Host Unreachable
^C
--- 192.168.203.119 ping statistics ---
4 packets transmitted, 0 received, +3 errors, 100% packet loss, time 3562ms
pipe 3
[root@Harry sendto]# ./sendto.exe 192.168.203.119 | more
connect OK
send successful
send successful
send successful
send successful
send successful
send successful
send successful
send successful
此时可以看到,在这个局域网中,其中的19.168.203.119这个地址是根本不存在的,但是UDP依然发送的非常欢乐,如果不是使用more让它休息一下,它会不知疲倦的一直打下去。这就说明这个UDP有多不靠谱了。
但是这种目标机不存在的伎俩对于TCP来说不能奏效,因为TCP的connect会进行三次握手。也就是TCP不会这么大大咧咧的就直接发送,然后快乐返回,而是首先进行试探性的接触,首先要接上头,然后才开始对话。虽然UDP也进行了connect操作,但是它不会向这个目的地址发送任何的消息来进行目的接触,事实上UDP的connect只是检查了目的主机的路由是否存在,然后存放了目的主机的地址,从而可以通过send而不是sendto来发送消息(也就是不需要每次都提供那个目的地址了)。
2、TCP测试
在之前的一片文章中分析过TCP的connect会阻塞 inet_stream_connect-->>>inet_wait_for_connect中,等待对方的回应。如果TCP在重传 指定次数之后没有收到报文,那么就会认为导致这个connect返回连接超时错误,从而不会进行接下来的发送操作了。
当然对于我们这些闲的蛋疼的人来说,这个答案肯定是不能让人满意的。那我们就来点重口味的:首先让连接成功,但是成功之后,在发送之前拔掉网线,当然关掉 自己的交换机或者路由器也可以,总之就是让网络物理不通,然后看这个send是否依然坚挺。由于我是在windows中的虚拟机里测试的,所以可以禁用掉 虚拟机的网卡就好了。
[root@Harry sockport]# more sockport.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <string.h>
main(argc, argv)
int argc; char **argv;
{
char hostname[100];
char dir[DIRSIZE];
int sd;
struct sockaddr_in sin;
struct sockaddr_in pin;
struct hostent *hp;
memset(&pin, 0, sizeof(pin));
pin.sin_family = AF_INET;
inet_pton(AF_INET,argv[1],&pin.sin_addr.s_addr);
pin.sin_port = htons(21);
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
/* connect to PORT on HOST */
if (connect(sd,(struct sockaddr *) &pin, sizeof(pin)) == -1) {
perror("connect");
exit(1);
}
printf("connectted\n");
int size = 0;
sleep(60);这里睡眠1分钟,让我有充分的时间拔掉网线。
printf("start loop\n");
while(1)
{
char buf[]="Helloworld";
/* send a message to the server PORT on machine HOST */
if (send(sd, buf,sizeof buf, 0) == -1) {
perror("send");
exit(1);
} else
printf("%d\n",size+=sizeof(buf));
}
/* wait for a message to come back from the server */
if (recv(sd, dir, DIRSIZE, 0) == -1) {
perror("recv");
exit(1);
}
/* spew-out the results and bail out of here! */
printf("%s\n", dir);
close(sd);
}
[root@Harry sockport]# ./sockport.exe 192.168.203.1
connectted
……
11572
11583
11594
最后执行的输出结果同样坑爹,可以看到TCP也发送的也很欢乐,但是它不像UDP那么无可救药,至少在发送了这么多内容之后停在那里,之后会返回网络超时。
看一下这个任务停在内核的哪个位置:
root 8828 0.0 0.0 792 192 pts/2 S+ 21:45 0:00 ./sockport.exe
root 8841 0.0 0.1 4988 1572 pts/3 Ss 21:47 0:00 bash
root 8859 0.0 0.0 4688 992 pts/3 R+ 21:47 0:00 ps aux
[root@Harry ~]# cat /proc/8828/wchan
sk_stream_wait_memory[root@Harry ~]#
可以看到,它是停止在了内核中的sk_stream_wait_memory中等待套接口的发送缓冲区了。
这个时间是多少呢?这个是受系统中配置参数的影响的,我的fedoracore12版本中,这个默认值设置为
[root@Harry sockport]# cat /proc/sys/net/ipv4/tcp_retries2
15
,但是它不是一个绝对时间值,而是一个重试次数。这个值比三次握手的重试次数要多很多次。这里的背后依据可能是既然TCP三次握手已经成功,网络中途出现问题的概率就比较小了,所以应该多次尝试,即使网络出问题了,也应该很快可以恢复吧。这个值大概在20到30分钟之间。
三、TCP报文发送
从这里的发送可以看到的是,调用TCP send接口的时候,TCP的报文并不是立即发送出本机的,而是经过了某些缓冲或者说延迟。其中最为重要的延时就是nagle算法,这个算法在系统中默认是打开的,也就是所有的TCP发送报文默认都是会经过该算法处理的(在tcp_v4_init_sock 函数中没有对tcp_sock结构的nonagle成员进行初始化,所以该值使用默认值0),当然依然可以通过setsockopt函数来禁用这个选项,参数时使用TCP_NODELAY。
在执行上面的二、2的TCP测试的时候,发现了一个有意思的现象,就是TCP将会不断的打印,然后我们现在的问题就是想看一下这个地方到底在什么时候会出现内存不足,也就是因为发送空间不足而将发送挂起。大家不要觉得这个问题简单而无聊,如果不看资料能够计算出这个值,需要涉及到很多的相关内容,大家有兴趣的话可以自己试一下。就像哥德巴赫猜想一样,这个问题的答案本身可能没有什么实际意义,但是在解决问题的过程中可能会引出很多副产品,例如有限域理论相关内容。或者说希尔伯特的23个数学问题一样,解决这些问题本身可能会引出很多副产品。下面是在我的电脑上执行这个程序的结果(这里要通过ethtool关闭网卡的GSO SG等选项的测试结果),下面是一个抓图结果,其实主要就是让大家眼见为实这个最后定格的发送数目11594,然后看一下这个值是怎么计算出来的:
1、tcp_sendmsg的两个重要的参数
在这个函数的开始,有两个比较重要的发送参数,分别为
mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
size_goal = tp->xmit_size_goal;
这里的mss_now是指一个TCP层有效负载的内容,所谓TCP有效负载,就能够承载的用户净数据,不包括TCP头、TCP option,IP头之类的东西。这个值受硬件限制,因为以太网设备都有一个MTU限制,就是一个报文的最大长度,这个可以认为是硬件物理设备决定的东西,MTU是只以太网的一个帧能够承载的用户净数据,并且MTU不包括以太网的两个地址(源MAC和目的MAC地址),可能是考虑到这个是以太网确定大小,所以就不计算入MTU了吧。而size_goal这个是指可以给网卡驱动一次最多多少数据,虽然以太网MTU可能比较小,但是在开启了GSO或者TSO的情况下,可以一次给网卡多个以太网帧(体现为一个sk_buff中有大量数据),从而让一些智能网卡具有更大的调度机会。这个size_goal一般是和mss_now相等的,但是如果打开了gso,那么这个size_goal可以达到一个很大值,大概为32KB数量级。为了便于分析,我手动关闭了网卡的这个gso tso sg选项,可以使用ethtool -k eth0确认自己的网卡是否已经关闭了这些选项,如果没有使用ethtool -K eth0 gso off之类选项关掉该选项。
对于mss_now的计算的主体函数为linux-2.6.21\net\ipv4\tcp_output.c:
/* Not accounting for SACKs here. */
int tcp_mtu_to_mss(struct sock *sk, int pmtu)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
int mss_now;
/* Calculate base mss without TCP options:
It is MMS_S - sizeof(tcphdr) of rfc1122
*/
这里的icsk->icsk_af_ops->net_header_len值初始化位于inet_connection_sock_af_ops ipv4_specific中,其值为sizeof(struct iphdr)=20字节。这个也是传输层和网络层分离的一个数据结构,因为同样是TCP,可能也有其他的网络层实现方式。
sizeof(struct tcphdr) 同样等于20字节。
mss_now = pmtu - icsk->icsk_af_ops->net_header_len - sizeof(struct tcphdr);
/* Clamp it (mss_clamp does not include tcp options) */
if (mss_now > tp->rx_opt.mss_clamp)
mss_now = tp->rx_opt.mss_clamp;
/* Now subtract optional transport overhead */
mss_now -= icsk->icsk_ext_hdr_len;这个值一般为零,至少在我的系统中如此。
/* Then reserve room for full set of TCP options and 8 bytes of data */
if (mss_now < 48)
mss_now = 48;
/* Now subtract TCP options size, not including SACKs */
mss_now -= tp->tcp_header_len - sizeof(struct tcphdr); tp->tcp_header_len 包含了tcp optisons和tcphdr两部分,由于前面已经减去了tcphdr的大小,所以这里需要再次补偿回来。在tcp_connect_init中,其中的可选长度根据是否添加时间戳sysctl_tcp_timestamps,是否使用MD5等功能有变长结构。我的测试系统中启用了tcp时间戳,但是没有MD5,所以添加了TCPOLEN_TSTAMP_ALIGNED=12字节。
[root@Harry sockport]# cat /proc/sys/net/ipv4/tcp_timestamps
1
return mss_now;
}
所以说这个计算大致的流程就是在MTU的基础上减去48个字节,其中TCPHDR和IPHDR各自20字节,然后TCP options中的时间戳使用12字节。那么这个MTU从哪里来呢?
我们在网卡初始化的时候,都会设置一个设备的MTU,例如,对于常用的以太网,它的setup函数可以为linux-2.6.21\net\ethernet\eth.c ether_setup函数中一般都初始化为
dev->mtu = ETH_DATA_LEN;
#define ETH_DATA_LEN 1500 /* Max. octets in payload */
也就是1500,所以当TCP发送报文的时候,mss(因此也就是xmit_goal_size)也就等于1448字节。
2、发送报文的合并
while (--iovlen >= 0) {
int seglen = iov->iov_len;
unsigned char __user *from = iov->iov_base;
iov++;
while (seglen > 0) {
int copy;
skb = sk->sk_write_queue.prev;这里的发送队列是下次向这个socket写入的时候需要写入的队列的结尾,也就是可以认为是下次要追加新的sk_buff的位置。
if (!sk->sk_send_head ||这里是发送队列的头部,这个写入队列和发送队列是可以不同的,假设说发送很慢,而写的很快,那么可能发送队列会远远落后于写入队列,但是不管怎样,write_queue始终是在send_head之后的。这里判断如果发送队列非空,那么就需要分配一个新的sk_buff结构了。
(copy = size_goal - skb->len) <= 0) {另外,如果一个sk_buff中累计的数量已经达到或者超过了size_goal,那么也可以申请一个新的sk_buff结构。因为达到这个目标,就说明这个sk_buff可以提交给网卡驱动了。
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/
if (!sk_stream_memory_free(sk))这里就进行了发送缓冲区判断,我们的阻塞也就是在这里发生的。
goto wait_for_sndbuf;
skb = sk_stream_alloc_pskb(sk, select_size(sk, tp),这里申请一个新的sk_buff结构,这里对于计算也很重要,这里具体申请了多少,有多少是用户可用的,有多少是向系统记账的,都是需要仔细分析的问题。
0, sk->sk_allocation);
3、sk_buff的内存申请与记账
skb = sk_stream_alloc_pskb(sk, select_size(sk, tp),
0, sk->sk_allocation);
其中的select_size申请的就是mss大小的空间,也就是1448字节的空间,也就是tcp一次申请的最小内存数量。
static inline struct sk_buff *sk_stream_alloc_pskb(struct sock *sk,
int size, int mem,
gfp_t gfp)
{
struct sk_buff *skb;
int hdr_len;
hdr_len = SKB_DATA_ALIGN(sk->sk_prot->max_header);这里的 sk->sk_prot->max_header在 中初始化为tcp_prot .max_header = MAX_TCP_HEADER,也就是#define MAX_TCP_HEADER (128 + MAX_HEADER)字节。这个MAX_HEADER宏定义比较复杂,但是看一下linux-2.6.21\arch\i386\defconfig之后,这些值都是可以确定的,相关变量只有CONFIG_IPV6_SIT是使能的,所以这个总大小为128 + 48 + 32 =208字节。然后是SKB_DATA_ALIGN,里面使用的CONFIG_X86_L1_CACHE_SHIFT同样在defconfig中定义为7,所以说208这个结构将会按照(1<<7)128字节对齐,对齐之后的结果为256字节。所以在接下来的size+hdr_len中将会申请1448+256=1704字节空间。
skb = alloc_skb_fclone(size + hdr_len, gfp);size + hdr_len=1704
if (skb) {
skb->truesize += mem;
if (sk_stream_wmem_schedule(sk, skb->truesize)) {
skb_reserve(skb, hdr_len);这里也很重要,虽然额外申请了这么多的空间,但是事实上这个sk_stream_alloc_pskb直接私吞了自己额外申请的hdr_len大小,所以当这个函数返回之后,用户依然只能看到其中原始声明的1448字节空间。
return skb;
}
__kfree_skb(skb);
然后上面的调用将会执行到alloc_skb_fclone-->>__alloc_skb--->>>
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int fclone, int node)
{
……
size = SKB_DATA_ALIGN(size);这里再次进行了对齐,1704按照128字节对齐为1792。
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
gfp_mask, node);
if (!data)
goto nodata;
memset(skb, 0, offsetof(struct sk_buff, truesize));
skb->truesize = size + sizeof(struct sk_buff);这里的truesize将会在size(1792)基础上再加上一个sk_buff结构(152字节),所以此时的truesize的大小为1944字节。这里是真正的算入一个sk_buff申请空间的内容,可以看到,虽然有效负载mss的值为1448,但是系统记账的时候计入的是1944字节。
atomic_set(&skb->users, 1); 下面的内容对这里的理解并没有多大关系,但是对之后理解sk_buff的组织结构比较有帮助。可以看到的是,sk_buff结构是使用专用的slab队列申请的,他只是一个sk_buff结构,而它的数据(data)则是直接从系统中分配的,也就是说,sk_buff和自己的数据并不是逻辑相连的,但是此时依然把sk_buff算在了这个sk_buff结构中。
skb->head = data;
skb->data = data;
skb->tail = data;
skb->end = data + size;这里就可以看到sk_buff的tail和end的区别:tail是一个一个动态变化的内容,随着数据的写入会不断的后移,表示sk_buff中有效数据的尾部定界,而end则是确定的指向一个skb_shared_info结构。
……
}
然后在int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,函数的最内层循环中,此时看一下系统向用户charge了多少内存:
skb_entail(sk, tp, skb);--->>>sk_charge_skb(sk, skb);
static inline void sk_charge_skb(struct sock *sk, struct sk_buff *skb)
{
sk->sk_wmem_queued += skb->truesize;
也就是系统是按照各层拿了回扣之后的真正大小,也就是前面看到的1944字节。
简言之,每个sk_buff申请的时候,自己可用得静负载为1448字节,而系统记账(加上中间各层雁过拔毛用掉的)1944字节。
4、发送报文的延迟发送
为了减少系统中发送小包的数目,系统一般为根据nagle算放来延迟一些小包的发送,从而假设网络速度很慢,此时发送又比较快,那么由于有下一个小包到来的时候就可以追加在前一个报文的空载mss中,从而可以提高有效负载,减少系统中断次数。wikipedia(http://en.wikipedia.org/wiki/Nagle's_algorithm)中对于这个算法的描述为
if there is new data to send
if the window size >= MSS and available data is >= MSS 如果说发送数据大于一个mss,那么就可以向网卡驱动中提交这个sk_buff结构。
send complete MSS segment now
else
if there is unconfirmed data still in the pipe 如果说有一些报文已经发送出去,但是还没有得到回应,那么同样暂时不发送报文。
enqueue data in the buffer until an acknowledge is received
else
send data immediately直接发送报文。这里对于上面的if,有一个特殊情况,就是如果是第一次发送,那么无论mss和data之间什么关系,都要发送。因为系统还没有向网络中发包,所以更不会有未确认包得问题了。这个对之后计算阻塞的时候同样是必须的。
end if
end if
end if
然后看系统中的实现代码
tcp_sendmsg--->>>tcp_push--->>>__tcp_push_pending_frames--->>>>tcp_write_xmit--->>>tcp_nagle_test--->>>tcp_nagle_check
static inline int tcp_minshall_check(const struct tcp_sock *tp)
{
return after(tp->snd_sml,tp->snd_una) &&
!after(tp->snd_sml, tp->snd_nxt);
}
/* Return 0, if packet can be sent now without violation Nagle's rules:如果返回值非零,那么报文将会被延迟发送。
* 1. It is full sized.
* 2. Or it contains FIN. (already checked by caller)
* 3. Or TCP_NODELAY was set.
* 4. Or TCP_CORK is not set, and all sent packets are ACKed.
* With Minshall's modification: all sent small packets are ACKed.
*/
static inline int tcp_nagle_check(const struct tcp_sock *tp,
const struct sk_buff *skb,
unsigned mss_now, int nonagle)
{
return (skb->len < mss_now &&如果sk_buff中已经累计了大于mss字节数据,则可以发送。
((nonagle&TCP_NAGLE_CORK) ||如果设置了TCP_NAGLE_CORK则必须等到sk_len足够大时发送。
(!nonagle &&如果nonagle非零,由于此时是默认值0,所以这个无效。
tp->packets_out &&如果这个socket已将向外发送过报文,(第一个报文发送将在这里返回0,及第一个报文始终可以发送)
tcp_minshall_check(tp))));这个检测从上面的代码可以看到,条件是这个套接口中有一个小包没有被确认,即落在了una(未确认)到已发送(snd_nxt)之间。所谓的小包就是指其中的有效负载数据小于mss的报文。这个值在tcp_minshall_update中更新。
}
5、何时发送
这样报文被延迟发送始终是个问题,那么这些延时的报文再什么时候被真正发送出去呢?如果用户发送了两个小包,第一个发送出去未确认,第二个小包将会一直等待,如果之后再也不发送数据,从而sk_buff的值永远无法大于mss了,第二个小包要何时发送?
这个正所谓解铃还须系铃人,既然之前判断是由于有已发送的小包还没有确认,那么一定是推迟到小包确认的时候了。这个和很多硬件的驱动使用的都是相同的模式:假设说当前驱动正在执行一个写操作,那么下一次写操作就会推迟到本次写操作完成的时候来执行本次写操作。那么具体的第二个小包的发送代码为
tcp_v4_do_rcv--->>>tcp_rcv_established--->>>tcp_data_snd_check--->>>tcp_push_pending_frames--->>__tcp_push_pending_frames
此时由于已经没有未确认小包数据了,甚至tp_>packets_out也已经为空,所以第二报文就可以被发送出去了。
6、根据上面的描述将计算总结一下
①、第一个报文发送的时、tcp_sendmsg中 if (!sk->sk_send_head判断满足,所以分配一个报文,在发送的时候,由于tp->packets_out为零,所以满足 tcp_nagle_check条件,第一个报文被发送出去。报文发送出去之后,sk->sk_send_head再次被清空。此时系统记账1944字节,用户态看到发送大小为11字节。
②、第二个报文发送同样满足sk_send_head为空,再次申请,但是由于第一个包没有被确认,所以这个包被延迟发送,此时申请新的sk_buff,系统记账1944,实际可用负载为mss 1448,此时占用11字节。
③、之后发送只有当一个sk_buff累积到达1448字节之后才会申请新的sk_buff,因为send_head非零。
④、系统默认写缓冲区大小
[root@Harry sockport]# cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 3493888
也就是16384字节。
现在第一次发送之后剩余16384-1944=14440。 14440/1944=7.4,也就是这些空间能够满足7.4次sk_buff申请,每次申请的一个sk_buff负载为1448,现在的问题是第9个sk_buff能否申请成功?
现在假设申请了8个(包括第一个只有11负载的报文),系统记账1944×8=15552,此值小于套接口的16384限制,再次进入tcp_sendmsg函数时
if (!sk_stream_memory_free(sk))
goto wait_for_sndbuf;
skb = sk_stream_alloc_pskb(sk, select_size(sk, tp),
0, sk->sk_allocation);
此时的第一个判断sk_stream_memory_free依然是满足的,所以会再次分配sk_buff,所以总共可以分配得到9个sk_buff,第10个将会触发超支。这个9个sk_buff总共负载为1448×8=11584字节,其中第一个只负载了11字节,所以可以负载的字节数为11 + (11584/11)*11=11+11583=11594,接下来的一次发送将会触发内存超支。
四、回答一下题目
到最后,发现已经离主题很远了。但是还是总结说明一下,虽然send是可靠传输,但是返回成功并不意味着数据已经发送出主机,更不用说已经被对方接受到。所以进行网络编程的时候不要一个人在那里滔滔不绝,而不管对方是不是已经睡着或者已经离场了。要和接收方有一定的交互,比如发送一段时间之后从对方接受一下,从而有必要的话进行自我阻塞。
五、TODO
其实确认报文(ACK)也有这个问题,就是当一个TCP报文到来之后,可能让一个数据报文捎带(piggyback)回去一个确认报文,避免出现空载确认,当然这里同样还要考虑到具体等多上时间才能搭上顺风车,如果没有,什么时候如何触发空载的专门确认?
主机什么时候会主动丢包
标签:发送,skb,网络协议,tcp,TCP,sk,send,sendto,buff 来源: https://www.cnblogs.com/tsecer/p/10485979.html