linux发送tcp请求_linux查询当前tcp连接请求 - CSDN
精华内容
参与话题
  • linux 内核tcp数据发送的实现

    千次阅读 2013-08-20 13:51:00
    在分析之前先来看下SO_RCVTIMEO和SO_SNDTIMEO套接口吧,前面分析代码时没太注意这两个.这里算是个补充.  SO_RCVTIMEO和SO_SNDTIMEO套接口选项可以给套...可是我在阅读内核源码的过程中看到,在linux中,accept和connect
    在分析之前先来看下SO_RCVTIMEO和SO_SNDTIMEO套接口吧,前面分析代码时没太注意这两个.这里算是个补充. 

    SO_RCVTIMEO和SO_SNDTIMEO套接口选项可以给套接口的读和写,来设置超时时间,在unix网络编程中,说是他们只能用于读和写,而像accept和connect都不能用他们来设置.可是我在阅读内核源码的过程中看到,在linux中,accept和connect可以分别用SO_RCVTIMEO和SO_SNDTIMEO套接口来设置超时,这里他们的超时时间也就是sock的sk_rcvtimeo和sk_sndtimeo域.accept和connect的相关代码我前面都介绍过了,这里再提一下.其中accept的相关部分在inet_csk_accept中,会调用sock_rcvtimeo来取得超时时间(如果是非阻塞则忽略超时间).而connect的相关代码在inet_stream_connect中通过调用sock_sndtimeo来取得超时时间(如果非阻塞则忽略超时时间). 

    --------------------------------------------------------------------------------- 
    tcp发送数据最终都会调用到tcp_sendmsg,举个例子吧,比如send系统调用. 

    send系统调用会z直接调用sys_sendto,然后填充msghdr数据结构,并调用sock_sendmsg,而在他中,则最终会调用__sock_sendmsg.在这个函数里面会初始化sock_iocb结构,然后调用tcp_sendmsg. 

    在sys_sendto中还会做和前面几个系统调用差不多的操作,就是通过fd得到socket,在sock_sendmsg中则会设置aio所需的操作. 

    我们简要的看下__sock_sendmsg的实现.可以看到在内核中数据都是用msghdr来表示的(也就是会将char *转为msghdr),而这个结构这里就不介绍了,unix网络编程里面有详细的介绍.而struct kiocb则是aio会用到的. 

    Java代码  收藏代码
    1. static inline int __sock_sendmsg(struct kiocb *iocb, struct socket *sock,  
    2.                  struct msghdr *msg, size_t size)  
    3. {  
    4.     struct sock_iocb *si = kiocb_to_siocb(iocb);  
    5.     int err;  
    6.   
    7.     si->sock = sock;  
    8.     si->scm = NULL;  
    9.     si->msg = msg;  
    10.     si->size = size;  
    11.   
    12.     err = security_socket_sendmsg(sock, msg, size);  
    13.     if (err)  
    14.         return err;  
    15.   
    16. ///这里就会调用tcp_sendmsg.  
    17.     return sock->ops->sendmsg(iocb, sock, msg, size);  
    18. }  


    我们在前面知道tcp将数据传递给ip层的时候调用ip_queue_xmit,而在这个函数没有做任何切片的工作,切片的工作都在tcp层完成了.而udp则是需要在ip层进行切片(通过ip_append_data). 而tcp的数据是字节流的,因此在 
    tcp_sendmsg中主要做的工作就是讲字节流分段(根据mss),然后传递给ip层. 可以看到它的任务和ip_append_data很类似,流程其实也差不多. 所以有兴趣的可以看下我前面的blog 


    而在tcp_sendmsg中也是要看网卡是否支持Scatter/Gather I/O,从而进行相关操作. 



    下面我们来看它的实现,我们分段来看: 



    Java代码  收藏代码
    1. ///首先取出句柄的flag,主要是看是非阻塞还是阻塞模式.  
    2. flags = msg->msg_flags;  
    3. ///这里取得发送超时时间.  
    4.     timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);  
    5.   
    6. ///如果connect还没有完成则等待连接完成(如是非阻塞则直接返回).  
    7.     if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))  
    8.         if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)  
    9.             goto out_err;  
    10.   
    11.     /* This should be in poll */  
    12.     clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);  
    13. ///取出当前的mss,在tcp_current_mss还会设置xmit_size_goal,这个值一般都是等于mss,除非有gso的情况下,有所不同.这里我们就认为他是和mms相等的.  
    14.     mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));  
    15.     size_goal = tp->xmit_size_goal;  



    在取得了相关的值之后我们进入循环处理msg,我们知道msghdr有可能是包含很多buffer的,因此这里我们分为两层循环,一层是遍历msg的buffer,一层是对buffer进行处理(切包或者组包)并发送给ip层. 

    首先来看当buf空间不够时的情况,它这里判断buf空间是否足够是通过 

    Java代码  收藏代码
    1. !tcp_send_head(sk) ||  
    2.                 (copy = size_goal - skb->len) <= 0  


    来判断的,这里稍微解释下这个: 

    这里tcp_send_head返回值为sk->sk_send_head,也就是指向当前的将要发送的buf的位置.如果为空,则说明buf没有空间,我们就需要alloc一个段来保存将要发送的msg. 

    而skb->len指的是当前的skb的所包含的数据的大小(包含头的大小).而这个值如果大于size_goal,则说明buf已满,我们需要重新alloc一个端.如果小于size_goal,则说明buf还有空间来容纳一些数据来组成一个等于mss的数据包再发送给ip层. 

    Java代码  收藏代码
    1.     /* Ok commence sending. */  
    2.     iovlen = msg->msg_iovlen;  
    3.     iov = msg->msg_iov;  
    4. ///copy的大小  
    5.     copied = 0;  
    6.   
    7.     err = -EPIPE;  
    8. ///如果发送端已经完全关闭则返回,并设置errno.  
    9.     if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))  
    10.         goto do_error;  
    11.   
    12.   
    13. while (--iovlen >= 0) {  
    14. ///取得当前buf长度  
    15.         int seglen = iov->iov_len;  
    16. ///buf的基地址.  
    17.         unsigned char __user *from = iov->iov_base;  
    18.         iov++;  
    19.         while (seglen > 0) {  
    20.             int copy;  
    21. ///我们知道sock的发送队列sk_write_queue是一个双向链表,而用tcp_write_queue_tail则是取得链表的最后一个元素.(如果链表为空则返回NULL).  
    22.   
    23.             skb = tcp_write_queue_tail(sk);  
    24.   
    25. ///上面介绍过了.主要是判断buf是否有空闲空间.  
    26.             if (!tcp_send_head(sk) ||  
    27.                 (copy = size_goal - skb->len) <= 0) {  
    28.   
    29. new_segment:  
    30. ///开始alloc一个新的段.  
    31.                 if (!sk_stream_memory_free(sk))  
    32.                     goto wait_for_sndbuf;  
    33. ///alloc的大小一般都是等于mss的大小,这里是通过select_size得到的.  
    34.                 skb = sk_stream_alloc_skb(sk, select_size(sk),  
    35.                         sk->sk_allocation);  
    36.                 if (!skb)  
    37.                     goto wait_for_memory;  
    38.                 /* 
    39.                  * Check whether we can use HW checksum. 
    40.                  */  
    41.                 if (sk->sk_route_caps & NETIF_F_ALL_CSUM)  
    42.                     skb->ip_summed = CHECKSUM_PARTIAL;  
    43. ///将这个skb加入到sk_write_queue队列中,并更新sk_send_head域.  
    44.                 skb_entail(sk, skb);  
    45. ///将copy值更新.  
    46.                 copy = size_goal;  
    47.             }  



    接下来如果走到这里,则说明 要么已经alloc一个新的buf,要么当前的buf中还有空闲空间. 
    这里先来分析alloc一个新的buf的情况. 

    这里先看下skb中的几个域的含义: 


     

    head and end 指的是alloc了的buf的起始和终止位置,而data and tail 指的是数据段的起始和终止位置,因此经过每一层tail和data都会变化的,而初始值这两个是相等的. 

    我们来看skb_tailroom,它主要是用来判断得到当前的skb的tailroom的大小.tailroom也就是当前buf的剩余数据段的大小,这里也就是用来判断当前buf是否能够再添加数据. 

    Java代码  收藏代码
    1. static inline int skb_is_nonlinear(const struct sk_buff *skb)  
    2. {  
    3.     return skb->data_len;  
    4. }  
    5. static inline int skb_tailroom(const struct sk_buff *skb)  
    6. {  
    7. ///如果是新alloc的skb则会返回tailroom否则返回0  
    8.     return skb_is_nonlinear(skb) ? 0 : skb->end - skb->tail;  
    9. }  


    接下来来看代码: 


    Java代码  收藏代码
    1. while (--iovlen >= 0) {  
    2. ...........................  
    3.         while (seglen > 0) {  
    4.   
    5. ///如果copy大于buf的大小,则缩小copy.  
    6.             if (copy > seglen)  
    7.                 copy = seglen;  
    8. ///这里查看skb的空间.如果大于0,则说明是新建的skb.  
    9.             if (skb_tailroom(skb) > 0) {  
    10. ///如果需要复制的数据大于所剩的空间,则先复制当前skb所能容纳的大小.  
    11.                 if (copy > skb_tailroom(skb))  
    12.                     copy = skb_tailroom(skb);  
    13. ///复制数据到sk_buff.大小为copy.如果成功进入do_fault,(我们下面会分析)  
    14.                 if ((err = skb_add_data(skb, from, copy)) != 0)  
    15.                     goto do_fault;  
    16.             }   



    如果走到这一步,当前的sk buff中有空闲空间 也分两种情况,一种是 设备支持Scatter/Gather I/O(原理和udp的ip_append_data一样,可以看我以前的blog). 


    另外一种情况是设备不支持S/G IO,可是mss变大了.这种情况下我们需要返回new_segment,新建一个段,然后再处理. 
    \我建议在看这段代码前,可以看下我前面blog分析ip_append_data的那篇.因为那里对S/G IO的设备处理切片的分析比较详细,而这里和那边处理基本类似.这里我对frags的操作什么的都是很简单的描述,详细的在ip_append_data那里已经描述过. 


    然后再来了解下PSH标记,这个标记主要是用来使接收方将sk->receive_queue上缓存的skb提交给用户进程.详细的介绍可以看tcp协议的相关部分(推功能).在这里设置这个位会有两种情况,第一种是我们写了超过一半窗口大小的数据,此时我们需要标记最后一个段的PSH位.或者我们有一个完整的tcp段发送出去,此时我们也需要标记pSH位. 


    Java代码  收藏代码
    1. while (--iovlen >= 0) {  
    2. ...........................  
    3.         while (seglen > 0) {  
    4. ...............................  
    5.   
    6. else {  
    7.   
    8.             int merge = 0;  
    9. ///取得nr_frags也就是保存物理页的数组.  
    10.                 int i = skb_shinfo(skb)->nr_frags;  
    11. ///从socket取得当前的发送物理页.  
    12.                 struct page *page = TCP_PAGE(sk);  
    13. ///取得当前页的位移.  
    14.                 int off = TCP_OFF(sk);  
    15. ///这里主要是判断skb的发送页是否已经存在于nr_frags中,如果存在并且也没有满,则我们只需要将数据合并到这个页就可以了,而不需要在frag再添加一个页.  
    16.                 if (skb_can_coalesce(skb, i, page, off) &&  
    17.                     off != PAGE_SIZE) {  
    18.                     merge = 1;  
    19.                 } else if (i == MAX_SKB_FRAGS ||  
    20.                        (!i &&  
    21.                        !(sk->sk_route_caps & NETIF_F_SG))) {  
    22. ///到这里说明要么设备不支持SG IO,要么页已经满了.因为我们知道nr_frags的大小是有限制的.此时调用tcp_mark_push来加一个PSH标记.  
    23.                     tcp_mark_push(tp, skb);  
    24.                     goto new_segment;  
    25.                 } else if (page) {  
    26.                     if (off == PAGE_SIZE) {  
    27. ///这里说明当前的发送页已满.  
    28.                         put_page(page);  
    29.                         TCP_PAGE(sk) = page = NULL;  
    30.                         off = 0;  
    31.                     }  
    32.                 } else  
    33.                     off = 0;  
    34.   
    35.                 if (copy > PAGE_SIZE - off)  
    36.                     copy = PAGE_SIZE - off;  
    37. .................................  
    38. ///如果page为NULL则需要新alloc一个物理页.  
    39.             if (!page) {  
    40.                     /* Allocate new cache page. */  
    41.                     if (!(page = sk_stream_alloc_page(sk)))  
    42.                         goto wait_for_memory;  
    43.                 }  
    44. ///开始复制数据到这个物理页.  
    45.                 err = skb_copy_to_page(sk, from, skb, page,  
    46.                                off, copy);  
    47.                 if (err) {  
    48. ///出错的情况.  
    49.                     if (!TCP_PAGE(sk)) {  
    50.                         TCP_PAGE(sk) = page;  
    51.                         TCP_OFF(sk) = 0;  
    52.                     }  
    53.                     goto do_error;  
    54.                 }  
    55.   
    56. ///判断是否为新建的物理页.  
    57.                 if (merge) {  
    58. ///如果只是在存在的物理页添加数据,则只需要更新size  
    59.                     skb_shinfo(skb)->frags[i - 1].size +=  
    60.                                     copy;  
    61.                 } else {  
    62. ///负责添加此物理页到skb的frags.  
    63.                     skb_fill_page_desc(skb, i, page, off, copy);  
    64.                     if (TCP_PAGE(sk)) {  
    65. ///设置物理页的引用计数.  
    66.                         get_page(page);  
    67.                     } else if (off + copy < PAGE_SIZE) {  
    68.                         get_page(page);  
    69.                         TCP_PAGE(sk) = page;  
    70.                     }  
    71.                 }  
    72. ///设置位移.  
    73.                 TCP_OFF(sk) = off + copy;  
    74.             }  



    数据复制完毕,接下来就该发送数据了. 

    这里我们要知道几个tcp_push,tcp_one_push最终都会调用__tcp_push_pending_frames,而在它中间最终会调用tcp_write_xmit,而tcp_write_xmit则会调用tcp_transmit_skb,这个函数最终会调用ip_queue_xmit来讲数据发送给ip层.这里要注意,我们这里的分析忽略掉了,tcp的一些管理以及信息交互的过程. 


    接下来看数据传输之前先来分析下TCP_PUSH几个函数的实现,tcp_push这几个类似函数的最后一个参数都是一个控制nagle算法的参数,来看下这几个函数的原型: 

    Java代码  收藏代码
    1. static inline void tcp_push(struct sock *sk, int flags, int mss_now,  
    2.                 int nonagle)  
    3.   
    4. void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,  
    5.                    int nonagle)  
    6.   
    7. static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)  


    我们还要知道tcp sock有一个nonagle域,这个域是会被tcp_cork套接口选项时被设置为TCP_NAGLE_CORK .先来看tcp_push的实现: 


    Java代码  收藏代码
    1. static inline void tcp_push(struct sock *sk, int flags, int mss_now,  
    2.                 int nonagle)  
    3. {  
    4.     struct tcp_sock *tp = tcp_sk(sk);  
    5.   
    6.     if (tcp_send_head(sk)) {  
    7.         struct sk_buff *skb = tcp_write_queue_tail(sk);  
    8. ///MSG_MORE这个参数我们在ip_append_data那里已经介绍过了,就是告诉ip层,我这里主要是一些小的数据包,然后ip层就会提前划分一个mtu大小的buf,然后等待数据的到来.因此如果没有设置这个或者forced_push返回真(我们写了超过最大窗口一般的数据),就标记一个PSH.  
    9.         if (!(flags & MSG_MORE) || forced_push(tp))  
    10.             tcp_mark_push(tp, skb);  
    11.         tcp_mark_urg(tp, flags, skb);  
    12. ///这里还是根据是否有设置MSG_MORE来判断使用哪个flags.因此可以看到如果我们设置了tcp_cork套接字选项和设置msg的MSG_MORE比较类似.最终调用tcp_push都会传递给__tcp_push_pending_frames的参数为TCP_NAGLE_CORK .  
    13.         __tcp_push_pending_frames(sk, mss_now,  
    14.                       (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle);  
    15.     }  
    16. }  


    在看tcp_write_xmit之前,我们先来看下tcp_nagle_test,这个函数主要用来检测nagle算法.如果当前允许数据段立即被发送,则返回1,否则为0. 

    Java代码  收藏代码
    1. ///这个函数就不介绍了,内核的注释很详细.  
    2.   
    3. /* Return 0, if packet can be sent now without violation Nagle's rules: 
    4.  * 1. It is full sized. 
    5.  * 2. Or it contains FIN. (already checked by caller) 
    6.  * 3. Or TCP_NODELAY was set. 
    7.  * 4. Or TCP_CORK is not set, and all sent packets are ACKed. 
    8.  *    With Minshall's modification: all sent small packets are ACKed. 
    9.  */  
    10. static inline int tcp_nagle_check(const struct tcp_sock *tp,  
    11.                   const struct sk_buff *skb,  
    12.                   unsigned mss_now, int nonagle)  
    13. {  
    14.     return (skb->len < mss_now &&  
    15.         ((nonagle & TCP_NAGLE_CORK) ||  
    16.          (!nonagle && tp->packets_out && tcp_minshall_check(tp))));  
    17. }  
    18. static inline int tcp_nagle_test(struct tcp_sock *tp, struct sk_buff *skb,  
    19.                  unsigned int cur_mss, int nonagle)  
    20. {  
    21. ///如果设置了TCP_NAGLE_PUSH则返回1,也就是数据可以立即发送  
    22.     if (nonagle & TCP_NAGLE_PUSH)  
    23.         return 1;  
    24.   
    25.     /* Don't use the nagle rule for urgent data (or for the final FIN). 
    26.      * Nagle can be ignored during F-RTO too (see RFC4138). 
    27.      */  
    28.     if (tcp_urg_mode(tp) || (tp->frto_counter == 2) ||  
    29.         (TCP_SKB_CB(skb)->flags & TCPCB_FLAG_FIN))  
    30.         return 1;  
    31. ///再次检测 nonagle域,相关的检测,上面已经说明了.  
    32.     if (!tcp_nagle_check(tp, skb, cur_mss, nonagle))  
    33.         return 1;  
    34.   
    35.     return 0;  
    36. }  


    然后看下tcp_write_xmit的实现, 

    Java代码  收藏代码
    1. static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)  
    2. {  
    3.     struct tcp_sock *tp = tcp_sk(sk);  
    4.     struct sk_buff *skb;  
    5.     unsigned int tso_segs, sent_pkts;  
    6.     int cwnd_quota;  
    7.     int result;  
    8. ///检测状态.  
    9.     if (unlikely(sk->sk_state == TCP_CLOSE))  
    10.         return 0;  
    11.   
    12.     sent_pkts = 0;  
    13.   
    14. ///探测mtu.  
    15.     if ((result = tcp_mtu_probe(sk)) == 0) {  
    16.         return 0;  
    17.     } else if (result > 0) {  
    18.         sent_pkts = 1;  
    19.     }  
    20.   
    21. ///开始处理数据包.  
    22.     while ((skb = tcp_send_head(sk))) {  
    23.         unsigned int limit;  
    24.   
    25.         tso_segs = tcp_init_tso_segs(sk, skb, mss_now);  
    26.         BUG_ON(!tso_segs);  
    27. ///主要用来测试congestion window..  
    28.         cwnd_quota = tcp_cwnd_test(tp, skb);  
    29.         if (!cwnd_quota)  
    30.             break;  
    31.   
    32.         if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))  
    33.             break;  
    34.   
    35.         if (tso_segs == 1) {  
    36. ///主要看这里,如果这个skb是写队列的最后一个buf,则传输TCP_NAGLE_PUSH给tcp_nagle_test,这个时侯直接返回1,于是接着往下面走,否则则说明数据包不要求理解发送,我们就跳出循环(这时数据段就不会被发送).比如设置了TCP_CORK.  
    37.             if (unlikely(!tcp_nagle_test(tp, skb, mss_now,  
    38.                              (tcp_skb_is_last(sk, skb) ?  
    39.                               nonagle : TCP_NAGLE_PUSH))))  
    40.                 break;  
    41.         } else {  
    42.             if (tcp_tso_should_defer(sk, skb))  
    43.                 break;  
    44.         }  
    45.   
    46.         limit = mss_now;  
    47.         if (tso_segs > 1 && !tcp_urg_mode(tp))  
    48.             limit = tcp_mss_split_point(sk, skb, mss_now,  
    49.                             cwnd_quota);  
    50.   
    51.         if (skb->len > limit &&  
    52.             unlikely(tso_fragment(sk, skb, limit, mss_now)))  
    53.             break;  
    54.   
    55.         TCP_SKB_CB(skb)->when = tcp_time_stamp;  
    56. ///传输数据给3层.  
    57.         if (unlikely(tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC)))  
    58.             break;  
    59.   
    60.         /* Advance the send_head.  This one is sent out. 
    61.          * This call will increment packets_out. 
    62.          */  
    63.         tcp_event_new_data_sent(sk, skb);  
    64.   
    65.         tcp_minshall_update(tp, mss_now, skb);  
    66.         sent_pkts++;  
    67.     }  
    68.   
    69.     if (likely(sent_pkts)) {  
    70.         tcp_cwnd_validate(sk);  
    71.         return 0;  
    72.     }  
    73.     return !tp->packets_out && tcp_send_head(sk);  
    74. }  



    然后返回来,来看刚才紧接着的实现: 

    Java代码  收藏代码
    1. while (--iovlen >= 0) {  
    2. ...........................  
    3.         while (seglen > 0) {  
    4. ...............................  
    5.   
    6. ///如果第一次组完一个段,则设置PSH.  
    7.             if (!copied)  
    8.                 TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;  
    9. ///然后设置写队列长度.  
    10.             tp->write_seq += copy;  
    11.             TCP_SKB_CB(skb)->end_seq += copy;  
    12.             skb_shinfo(skb)->gso_segs = 0;  
    13. ///更新buf基地址以及复制的buf大小.  
    14.             from += copy;  
    15.             copied += copy;  
    16. ///buf已经复制完则退出循环.并发送这个段.  
    17.             if ((seglen -= copy) == 0 && iovlen == 0)  
    18.                 goto out;  
    19. ///如果skb的数据大小小于所需拷贝的数据大小或者存在带外数据,我们继续循环,而当存在带外数据时,我们接近着的循环会退出循环,然后调用tcp_push将数据发出.  
    20.             if (skb->len < size_goal || (flags & MSG_OOB))  
    21.                 continue;  
    22. ///forced_push用来判断我们是否已经写了多于一半窗口大小的数据到对端.如果是,我们则要发送一个推数据(PSH).  
    23.             if (forced_push(tp)) {  
    24.                 tcp_mark_push(tp, skb);  
    25. ///调用__tcp_push_pending_frames将开启NAGLE算法的缓存的段全部发送出去.  
    26.                 __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);  
    27.             } else if (skb == tcp_send_head(sk))  
    28. ///如果当前将要发送的buf刚好为skb,则会传发送当前的buf  
    29.                 tcp_push_one(sk, mss_now);  
    30.             continue;  
    31.   
    32. wait_for_sndbuf:  
    33.             set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);  
    34. wait_for_memory:  
    35.             if (copied)  
    36. ///内存不够,则尽量将本地的NAGLE算法所缓存的数据发送出去.  
    37.                 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);  
    38.   
    39.             if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)  
    40.                 goto do_error;  
    41. ///更新相关域.  
    42.             mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));  
    43.             size_goal = tp->xmit_size_goal;  
    44.         }  
    45.     }  



    最后来看下出错或者成功tcp_sendmsg所做的: 

    Java代码  收藏代码
    1. out:  
    2. ///这里是成功返回所做的.  
    3.     if (copied)  
    4. ///这里可以看到最终的flag是tp->nonagle,而这个就是看套接口选项是否有开nagle算法,如果没开的话,立即把数据发出去,否则则会村讯nagle算法,将小数据缓存起来.  
    5.         tcp_push(sk, flags, mss_now, tp->nonagle);  
    6.     TCP_CHECK_TIMER(sk);  
    7.     release_sock(sk);  
    8.     return copied;  
    9.   
    10. do_fault:  
    11.     if (!skb->len) {  
    12. ///从write队列unlink掉当前的buf.  
    13.         tcp_unlink_write_queue(skb, sk);  
    14. ///更新send)head  
    15.         tcp_check_send_head(sk, skb);  
    16. ///释放skb.  
    17.         sk_wmem_free_skb(sk, skb);  
    18.     }  
    19.   
    20. do_error:  
    21.     if (copied)  
    22. ///如果copied不为0,则说明发送成功一部分数据,因此此时返回out.  
    23.         goto out;  
    24. out_err:  
    25. ///否则进入错误处理.  
    26.     err = sk_stream_error(sk, flags, err);  
    27.     TCP_CHECK_TIMER(sk);  
    28.     release_sock(sk);  
    29.     return err;  
    展开全文
  • Linux| |对于TCP的学习

    千次阅读 2019-02-20 20:26:26
    对于TCP的学习 前言 TCP称为“传输控制协议”。也就是要对数据的传输进行一个详细的控制   1. TCP的特点及其目的 为了通过IP数据报实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复、以及分...

    对于TCP的学习


    前言

    TCP称为“传输控制协议”。也就是要对数据的传输进行一个详细的控制

     

    1. TCP的特点及其目的

    为了通过IP数据报实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复、以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输

    TCP通过检验和、序列号、确认应答、重发机制、连接管理以及窗口控制等机制实现可靠传输

    有连接:使用TCP协议进行通信时,需要先建立连接

    可靠传输:具有确认应答机制,超时重传等机制保证数据的可靠传输

    面向字节流:传输的数据是字节流,没有长度的限制

    连接的概念

    • 在内核中有一个结构体来描述连接TCB,在通过队列将其管理起来

    • 操作系统维护一个连接是需要成本的(时间成本和空间成本)

    • 必须要使用合理的方式管理连接否则会导致服务器挂掉

    • 连接的数量是有上限的

     

    2. TCP协议段格式

    插图:TCP协议报文格式

     

    2.1 16位源端口号

    表示数据从哪个进程来

     

    2.2 16位目的端口号

    要到那个进程去

     

    2.3 32位序号

    序号是可靠传输的关键因素。TCP将要传输的每个字节都进行了编号,序号是本报文发送的数据段的第一个字节的编号,序号可以保证传输信息的有效性。

    • 序列号是按顺序给发送数据的每一个字节都标上号码的编号。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答(ACK)返送出去

      • 【注意】:给每一个字节都标上号码也就是,对于头部数据也要标上号码,也就是对于ACK和FIN这种标志也会有着自己的序号

    • 序列号的初始值并非是0,而是在建立连接以后由随机数生成,而后面的计算则是对每一字节加一

    • 序号也指字节与字节之间的间隔

    • 假设主机A的一个进程向主机B的一个进程发送一个数据流,主机A将隐式的对数据流中的每一个字节进行顺序编号。假定数据流有一个包含大于2000字节的文件组成,其MSS(最大报文长度)为1000字节,数据流的首编号是1,如下图:

    • 插图:发送数据

    • 给第一个报文段分配序号1,第二个报文段分配序号1001,依次类推,每一个序列号被填入到相应TCP报文段首部的序号字段中

    • 插图:序号和确认序号

    • TCP的数据长度没有写入TCP的首部。实际通信中求得TCP包的长度的计算公式是: IP首部中的数据报长度 - IP首部长度 - TCP首部长度

     

    2.4 32位确定序号

    每一个ACK(确认应答)对应着这一个确认号,他指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认序号只有当ACK标志为1时才有效。

    • 确定序号是根据接收数据首部中的序号和数据的长度来计算得到的。

    • 确定序号 = 首部序号 + MSS(最大消息长度)

    • TCP是全双工的,即主机A在向主机B发送数据的同时,也许也在接收来自主机B的数据。从主机B到达的每个报文段中都有一个序号用于从B流向A的数据。主机A填充进报文段的确认号是主机A期望从主机B收到的下一字节的序号,

    • 举例子说明:

    假设主机A已经收到了来自主机B的编号为0-535的所有字节,同时假设它打算发送一个报文段给主机B,主机A等待主机B的数据流中字节536及其后的所有字节,所以主机A会在它发往主机B的报文段的确认号字段中填上536。

    • 再举一个例子

    有关失序到达,并且TCP提供的是累计确认

    • 累计确认

      • 累计确认是一种差错控制技术,用于对接收报文的确认。在累计确认技术中,如果收到了后面报文的确认信息,前面的报文肯定已经接收正确,即便以后再收到前面报文的确认信息,也不需要处理了

    假设主机A已收到主机B的包含字节0-535字节的报文段,以及另一个包含字节900-1000的报文段。由于某种原因,主机A还没有收到字节536-899的报文段。在这个例子里,主机A为了重新构建主机B的数据流,仍在等待字节536(和其后的字节)。因此,A到B的下一个报文段将在确认号字段中包含536。因为TCP只确认该流中到第一个丢失字节为止的字节,所以TCP提供的是累积确认

    主机A虽然收到了字节900-1000的报文段,但是并不会在在一个发往主机B的报文段的确认号字段填1001,因为535后面的字节还没有得到确认,而受到的900-1000字节的报文段属于失序到达,对于失序到达的报文段的方法由TCP编程人员去具体实现,有两个基本选择:一是丢弃失序报文段,而是保留失序字节并等待缺少的字节以填补该间隔

     

    2.5 4位首部长度(数据偏移)

    表明该TCP头部有多少个32位bit(有多少个4字节),所以TCP头部最大长度是15 * 4 = 60。该部分可以将包头和有效载荷进行分离,TCP报文默认首部长度是20字节。所以选项可变长度最大是40字节

     

    2.6 6位保留位

    该字段是为了保留为了以后使用,一般设置为0,但即使收到的报文段中该字段不为0,也不丢弃

     

    2.7 6位标志位

    • URG:标志紧急指针是否有效,URG为1表示包中有需要紧急处理的数据

    • ACK:确认应答是否有效,为1则有效(确认号)

    • PSH:提示接收端应用程序立刻从TCP缓存区把数据读走。PSH为1是就是将缓存区中的数据交给上层应用,PSH为0,就是数据不需要立即传输然进行缓存

    • RST:为了处理异常连接的,告诉连接不一致的一方,我们的连接还没有建立好,要求对方重新建立连接。我们把携带RST标识的称为复位报文段

    • SYN:请求建立连接。我们称携带SYN标识的称为同步报文段

    • FIN:通知对方,本端要关闭了,我们称携带FIN表示的为结束报文段

     

    2.8 16位窗口大小

    如果发送方发送大量数据或者发送数据的速度过快,接收端来不及接收就会导致数据的大量丢失,所以接收端会和发送端协调,使其发送的数据能够来得及被处理。然后接收端可以发送给发送端消息让发送端发慢一点,这就是流量控制。

    接收方将自己接收缓存区剩余空间的大小告诉发送方。这个接收端缓存区剩余容量的大小就是16位窗口大小。发送方可以根据窗口大小来适配发送的速度和大小,窗口大小最大是2的16次方即64KB,但也可以根据选项中的某些位置扩展,最大扩展1G

     

    2.9 16校验和

    发送端填充,CRC校验。如果接收端校验不通过,则认为数据有问题(此处不仅校验TCP首部也校验TCP数据部分)

    • TCP与UDP校验和相似,只是TCP的校验和无法关闭,两者校验都要使用伪首部

    • TCP的伪首部和UDP基本相似,只不过UDP伪首部中的八位传输层协议号是17,而TCP是6

    • 插图:伪首部

    • 伪首部的作用:

      1. 仅在校验时使用

      2. 通过目的IP地址检验主机是否收错了报文

      3. 通过检验协议号查看是否交付给了正确的上层协议

    • IP首部可以检验IP地址为何还要伪首部进行检测

      • 当IP数据报在路由器中间进行转发时,可能会修改IP首部和其中的校验和,IP的首部可能会发生变动,所以需要进行二次检测,如果接收端校验和与发送端校验和所得出的结果不一致就会被丢弃

    • 检验方式:

      1. 把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。把TCP首部中的校验和字段置为0

      2. 用反码相加法累加所有的16位字(进位也要累加)

      3. 最后,对计算结果取反,作为TCP的校验和

     

    2.10 16位紧急指针

    按序到达是TCP协议保证可靠性的一种机制,但是也存在一些报文想要优先处理,这是就可以设置紧急指针,指向该报文即可,同时将紧急指针有效位(URG)置1。紧急指针指向了紧急数据的结尾部分的下一个字节。

    • 因为只有一个紧急指针,这也意味着他只能标识一个字节的数据,这个指针指向紧急数据最后一个字节的下一个字节。紧急指针也用作表示数据流分流的标志

    • 插图:紧急指针

    • 我们知道TCP传输数据时是有顺序的,他有字节号,URG配合紧急指针,就可以找到紧急数据的字节号。紧急数据的字节号公式如下:

    • 紧急数据字节号(urgSeq)= TCP报文序号(seq)+ 紧急指针(urgpoint)- 1

    • 如上图的例子:seq = 10,urgpoint = 5;那么自己序号urgSeq = 10 + 5 - 1 = 14

    • 一旦 TCP 知道了你要发送紧急数据,那么在接下来的数据发送中,TCP 会将所有的 TCP 报文段中的 URG 标志置位,哪怕该报文段中不包含紧急数据,这个行为会持续到紧急数据被发送出去为止。

    • 紧急指针会产生覆盖如果发送方多次发送紧急数据,最后一个数据的紧急指针会将前面的覆盖。比方说你发送了一个字节的紧急数据 'X',在 'X' 尚未被 TCP 发送前,你又发送了一个紧急数据 'Y',那么在后面的 TCP 报文中,紧急指针都是指向了 'Y' 的。

     

    2.11 选项

    用于提高TCP的传输性能,因为数据偏移(首部长度)进行控制,所以其长度最大是40字节。另外选项长度尽量调整为32位(4字节)的整数倍

    • 0,NOF选项表示结束选项,表明首部已经没有更多的消息了,数据从下一个32位开始,每个报文段只用一次,放在末尾用于填充

    • 1,NOP操作选项,一般用于将TCP选项的总长度填充为4字节的整数倍

    • 2,MSS(最大消息长度)4字节,用于连接时决定最大段长度的情况

    • 3,窗口扩大选项3字节,用来改善TCP吞吐量的选项。TCP首部中窗口字段只有16位。因此在TCP报的往返时间(RTT)最大只能发送64KB的数据。窗口的最大值可以扩展到1G字节。由此在一个RTT较长的网络环境中,也能达到较高的吞吐量

      • 3个字节中的一位表示移位S,新的窗口大小的值为(16+S),相当于把窗口左移S位,移位的上限为14,所以可以扩大的窗口的大小为65535 * 2 ^ 14即1GB

      • 窗口大小在连接建立的时候就确定了,连接后无法改变,如果已经实现了窗口的扩大,发送S = 0就可以恢复到16位的大小了

    • 4,选择性的应答2字节,TCP通信时,如果数据丢失,发送端会重发最后被确认序号后序的所有报文端,但这样会重复发送。使用选择性确认选项可以只发送丢失的数据,而不用重发所有未被确认的。在初始化连接时,可以选择是否选择SACK技术

    • 5,SACK的实际选项,该选项参数告诉发送端已经接收并且缓存了的数据块(已经接收的),可以是发送端并据此进行重发丢失的数据块,最大为40个字节,4组

    • 8,时间戳选项,当网络在传输大量的数据时,32位序号很快就会用完,用完序号就会重新开始,但当网络阻塞或者延迟较高是,就会造成新老序号在同一个网络中,就会造成混淆。时间戳就是为了区分新老序列号

     

    3. TCP以段为单位发送数据

    在建立TCP连接的同时,也可以确定发送数据包的单位,我们也可以称其为“最大消息长度”(MSS:Maximum Segment Size)。最理想的情况是,最大消息长度正好是IP中不会被分片处理的最大数据长度

    TCP在大量传输数据的时候,是以MSS的大小将数据进行分割发送的。进行重发也是以MSS为单位

    MSS是在三次握手的时候,在两端主机之间计算得出。两端主机在发出建立连接请求时,会在TCP首部中写入MSS选项,告诉对方自己的接口能够适应的MSS的大小。然后会在两者之间选一个较小的值投入使用。

    TCP协议传输时的MSS = Min(主机A中的MSS, 主机B中的MSS)

    为附加MSS选项,TCP首部将不再是20字节,而是4字节的整数倍。

    如下图:TCP的首部是24字节

    插图:具有MSS的TCP的报文首部

    【特例】

    • 在建立连接时,如果某一方的MSS选项被忽略,可以选为IP包的长度不超过576字节的值(例:IP首部 20 字节,TCP首部 20 字节,MSS 536 字节

     

    4. 确认应答机制

    在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已经收到的消息通知,这个消息就叫做确认应答(ACK)

    例子:在两个人谈话的时候,在谈话的停顿处可以点头或者询问以确认谈话内容。如果对方迟迟没有收到任何反馈,说话的一方还可以再重复一遍以保证对方确认收到。因此,对方是否听到了此次对方的内容要靠对方的反应来判断。

    当对方听懂对话内容时会说:“嗯”,这相当于返回了一个确认应答(ACK)。而当对方没有理解对话内容或没有听清回问一句“咦”,这就好比是一个否定确认应答(NACK)

    对于正常的数据传输:

    插图:正常的数据传输

    TCP通过肯定的确认应答(ACK)实现可靠的数据传输,当发送端发送数据之后会等待对方的确认应答,如果有确认应答说明数据成功到达对面,否则有可能数据丢失。

    • 该图就是主机A给主机B发送了序列号为1-1000的数据,ACK应答就会返回1001序列号,告诉主机A,已经收到了1-1000的数据,下一次从序列号为1001的字节开始发送数据

    • 为什么每次发送1000个字节的数据,这是由MSS(最大消息长度决定的)

    对于发送失败有两种情况:

    • 数据包丢失的情况:也就是发送端给接收端发送消息,没有发送过去

    插图:数据包丢失

    • 确认应答丢失的情况:也就是接收端接收到消息了,给发送端发送确认应答(ACK)消息,ACK丢失

    插图:确认应答丢失

    为了异常情况的产生之后还可以进行正常的通信就有了确认应答机制

     

    5. 超时重传机制

    对于上面产生的异常情况之后,主机A就会向主机B重发数据报。

    重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔.如果超过了这个时间仍未收到确认应答,发送端将进行数据重发。那么这个重发超时的具体时间长度是多少呢?

    • 最理想的就是找到一个最小的时间,他能保证“确认应答一定能在这个时间内返回“

    • 但是这个时间的长度会随着网络环境的变化而变化

      • TCP要求无论处在何种网络环境中都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。为此,他在每次发包时都会计算往返时间(RTT)和偏差。将这个往返时间(RTT)和偏差相加,重发时间就是比这个总和要稍微大一点的值

    • 如果超时时间设的太长,会引起整体的重传效率

    • 如果超时时间设的太短,有可能频繁的发送重复的数据报

    Linux(BSD的Unix和Windows)中,超时都以500ms(0.5秒)为单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。(对于最初的数据包还不知道往返时间,所以其重传时间一般设置为6秒左右

    如果重发一次还得不到应答则进行再次发送,等待应答的时间将会以2倍,4倍的指数函数延长

    另外数据也不会被无限、返回的重复重发。达到一定重发次数之后,如果仍没有确认应答(ACK)返回,TCP就会判定网络或者对端主机发生了异常强制关闭连接,并且通知应用通信异常强行终止

     

    6. 连接管理机制

    TCP是面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好通信两端的准备工作

    • 正常情况下,TCP要经历三次握手建立连接,四次挥手释放连接

     

    7. 理解TIME_WAIT状态

    首先我们做一个测试,首先启动server然后启动client,然后使用ctrl + c是server终止掉,这时再次运行server,结果是:

    插图:端口号绑定出错

    当我们将一个服务器关闭掉,再次重新启动服务器就会发现一个问题:就是不能马上再次绑定这个端口号,需要等一会儿才可以再次重新绑定。其实等的这一会儿就是断开连接的一方处于TIME_WAIT状态。所以会出现绑定失败。

    TCP协议规定,主动关闭连接的一方要出现TIME_WAIT状态,等待两个MSL(Maximum Segment Lifetime)的时间后才能回到CLOSED状态

    我们使用ctrl + c终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口

    MSL在RFE1122中规定为两分钟,但是各操作系统的实现不同,在Centos7上默认配置是60s

    • 可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout查看MSL的值

    • 首先解释一下MSL:MSL是报文最大生存时间,是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

    • 而对于网络层来说,对于IP协议报文首部中有一个TTL(time to live),中文可以译为生存时间,这个生存时间是由源主机设置初始值但不是生存的具体时间,而是一个存储了IP数据报可以经过我的最大路由数,没经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。

    • TLL与MSL是有关系的,但不是简单的相等的关系,MSL要大于等于TTL

    为什么TIME_WAIT的时间是2MSL?

    • 保证客户端发送的最后一个ACK(确认应答)报文段可以到达服务器端,这个ACK报文段可能丢失,因而是处于LAST_ACK端的服务器收不到ACK报文。这个时候服务器端就会给客户端重传FIN报文段,而客户端就能在2MSL时间内收到这个重传的FIN报文段,接着客户端继续发送ACK报文段交给服务器。然后客户端进入TIME_WAIT状态,重新启动2MSL。最后客户端和服务器一定都会进入到CLOSED状态。如果客户端没有2MSL,而是在发送完ASK(确认应答)报文后就直接进入到CLOSED状态,那么如果ACK在传输失败后,客户端就接收不到服务器重新发送的FIN报文段了,也就不会再给服务器端发送ACK(确认应答)报文段,这样就会导致服务器端,无法进入到CLOSED状态

    • 保证两个传输方向上尚未被接收到或者迟到的报文都已经消失。(否则服务器重启,可能收到来自上一个进程迟到的数据(FIN报文),但是这种数据很可能是错误的)。例:比如客户端发送第一个请求连接报文段丢失而未收到确认,客户端就会重传一次连接请求,第二次服务器端收到了确认,建立了连接。数据传输完毕后就释放了连接。客户端一共发送了两个连接请求报文段,其中第一个丢失了,第二个到达了服务器。假如客户端发送第一个连接请求报文段没有丢失,而是在某些网络结点长时间逗留了,以至于延误到连接释放后的某个时间才到达服务器端,这本来是已失效的报文段,但是服务器并不知道,就会又建立一次连接。而等待的2MSL就是为了解决这个问题,客户端在发送完最后一个确认应答(ACK)之后,在经过2MSL时间,就可以是本次连接诶持续时间内所产生的的所有报文段都从网络中消失,这样就可以使下一个新的连接不会出现这种旧的连接请求报文段了。

     

     

    8. 解决TIME_WAIT引起的bind失败的方法

    在2MSL时间内这个地址上的连接(客户端的端口和服务器端的端口)不能被使用。

    这是因为在这个2MSL时间内,客户端和服务器端的连接还是存在的所以不能给这个端口在此绑定上一个应用程序

    在server的TCP连接没有完全断开之前不允许重新绑定,也就是TIME_WAIT时间没有过,但是这样不允许绑定在某些情况下是不合理的:

    • 服务器需要处理大量的客户端连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求),这个时候如果由服务器主动关闭连接(比如某些客户不活跃,就需要被服务器daunt主动清理掉),这样服务器端就会产生大量TIME_WAIT状态。

    • 如果客户端的请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源IP,源端口,目的IP,目的端口,协议)。其中服务器的IP和端口以及协议是固定的,如果新来的客户端连接的IP和端口号处于TIME_WAIT状态而无法连接到服务器。

    解决方法:

    使用setsockopt()函数就可以了。设置socket第三个参数设置为SO_REUSEADDR为1,表示允许创建端口号一样但IP地址不同的多个socket描述符

    使用方法:https://blog.csdn.net/qq_40399012/article/details/85983221

     

    9. 理解CLOSE_WAIT状态

    如果客户端是主动断开连接的一方,客户端给服务器发送一个FIN报文段,服务器端就要给客户端发送一个ACK(确认应答),但是在服务器端假设没有关闭连接(也就是没有调用close()函数去关闭socket描述符),这是服务器就会产生一个CLOSE_WAIT状态,因为服务器没有去关闭连接

    大家可以去看一下这份代码:

    https://github.com/YKitty/LinuxDir/tree/master/LinuxCode/netWork/TCP/ThreadPoolTCP

    中的TcpServer.hpp将其中的Service成员函数中的close(new_sock)取消掉就可以看到服务器端进入到了CLOSE_WAIT状态了

    操作顺序:编译运行服务器,启动客户端连接查看TCP状态,客户端和服务器端度为ESTABLELISHED状态,没有问题,然后我们关闭客户端程序,观察TCP状态,看到结果如下图。

    观察TCP状态使用命令netstat:https://blog.csdn.net/qq_40399012/article/details/85983221#t19

    运行结果:

    插图:服务器端进入到CLOSE_WAIT状态

    此时服务器进入到了CLOSE_WAIT状态,结合四次挥手的流程图,可以认为四次回收还没有正确完成

    【注意】:对于服务器出现大量的CLOSE_WAIT状态,原因就是服务器没有正确关闭socket,导致四次回收没有完成这是一个BUG,只需要加上对应的close()函数就好了

     

    10. 滑动窗口

    确认应答策略的每一个发送的数据段都要给一个ACK确认应答,接收方收到ACK后再发送下一个数据段,但是这样做有一个比较大的缺点,就是性能差,尤其是数据往返的时间较长的时候

    既然一收一发的方式性能较低,那么我们考虑一次发送多条数据,就可以大大的提高性能(其实就是将多个段的等待时间重叠在了一起)

    为解决这个问题。TCP引入了窗口这个概念。即使在往返时间较长的情况下,他也能控制网络性能下降。

    如图:确认应答不再是一个段了,而是一个更大的单位(窗口)。也就是说发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送

    插图:用滑动窗口并行处理

    窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。如上图:窗口大小分为4个段(4000字节)。

    插图:滑动窗口方式

    如上图,在窗口的数据即使没有收到确认应答也可以发出去。当收到第一个ACK(确认应答)之后滑动窗口向右移动,继续发送第五个段的数据,然后依次类推。操作系统内核为了维护这个滑动窗口,需要开辟发送缓存区来记录当前有哪些数据没有应答,只有应答过的数据才能从缓存区中删除

    滑动窗口越大,网络的吞吐率越高

    滑动窗口左边表示已经发送过并且确认的数据,可以从缓存区中删除,滑动窗口里面表示可以发送的数据或者发送出去但是还没有确认,滑动窗口右边代表还没有发送的数据

    如果使用窗口控制中,出现段丢失该怎么办呢?

    情况一:数据报已经到达,ACK丢失了

    数据已经达到了对端,是不需要再进行重发的。然而在没有使用窗口控制的时候,没有收到确认应答的数据会被重发,而使用了窗口控制,某些确认应答即便丢失了也不需要重发

    如图:

    插图:没有确认应答也不受影响

    如图对于ACK(确认应答)1001序号的应答没有发送到客户端,但是只要1001序号后面的确认应答发送到了客户端,前面的确认应答没有到客户端也没有关系,客户端依旧是正确发送数据的。这是因为:只有服务器端接收到了前面的报文段才会给客户端发送自己希望下一次发送过来的序列号增加,否则的话就会一直放松希望下一个序列号是前面的。

    情况二:某个报文段丢失的情况

    插图:高速重发控制

    接收主机如果收到一个自己应该受到的序号以外的数据时,会针对当前为止收到数据返回确认应答。

    如图当某一段报文段丢失后,发送端会一直收到序号为1001确认应答,这个确认应答好像是在提醒发送端“我想接收的是从1001开始的数据”。在窗口比较大,又出现报文丢失的情况下,同一个序号的确认应答将会被重复不断的返回。而发送端主机如果连续三次收到同一个确认应答,就会将其所对应的数据进行重发,这时接收端收到1001序号开始的报文段了,之后,再次返回的就是ACK(确认应答)为7001了,因为2001 - 7001接收端已经收到了,被放到接收端操作系统内核的接收缓存区了。这种机制高速重发机制(也叫“快重传”)

    快重传要求接收方在收到有一个失序的报文段后就立即发出重复确认(为的是使发送方鸡早知道有报文段没有到达对方)而不需要等到自己发送数据时捎带确认。快重传规定,发送方只要一连收到三个重复确认应答就应当立即重传对方尚未收到的报文段,而不必等待设置的重传计时器时间到期。由于不需要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提高整个网络的吞吐量。

    快重传:

    • 发送端收到三个连续的确认应答,就立即重发数据,不需要等待重传时间

    • 接收端只要收到一个失序的报文段就立即发出三次确认应答,不需要等待捎带应答

     

     

    11. 流量控制

    出现的原因:

    • 发送段根据自己的实际情况发送数据。但是接收端可能收到的是一个毫无关系的数据包有可能会在处理其他问题上花费一些时间。因此在为这个数据包作其他处理耗费一些时间,甚至在高负荷的情况下无法接受任何的数据。如此一来如果接收端将本该接收的数据丢弃的话,就又会触发重发机制,从而导致网络流量的无端浪费

    为了防止该现象的产生TCP提供了一种机制可以让发送端的根据接收端的实际接受能力控制发送的数据量。这就是流量控制。

    接收端主机向发送端主机通知自己可以接受数据的大小,于是发送端会发送不超过这个限度的数据。该限度就是窗口大小。前面的窗口大小就是由接收端主机决定的

    TCP首部中有一个专门的字段(窗口)存储窗口大小。接收主机将自己的接收缓存区剩余的大小放到该字段中通过ACK(确认应答)发送给客户端

    窗口大小字段越大,说明网络吞吐量越高

    接收端一旦发现自己的缓存区快满了,就会将窗口大小设置成一个更小的值通知给发送端

    发送端主机会根据接收端主机的提示,对发送数据的量进行控制,减慢自己的发送速度

    如果接受缓存区满了就会将窗口置为0,这时发送端不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端

    插图:流量控制

    如图:当接收端收到从3001号开始的数据段后,其缓存区即满,不得不暂时停止接收数据。之后,在收到窗口更新通知后通信才得以继续进行。如果这个窗口的更新通知在传输途中丢失,可能会导致无法继续通信。为避免这类问题的发生,发送端主机会时不时的发送一个叫做窗口探测的数据段,此数据端仅包含一个字节以获取最新的窗口大小

    什么时候发送端发送窗口探测数据段?

    • 过了重发超时的时间还没有收到接收端窗口更新的通知,发送端就会发送一个窗口探测更新

     

    12. 拥塞控制

    虽然TCP有了窗口控制,收发主机即使不再以一个数据为单位发送确认应答,也能够连续发送大量数据包。然而在通信刚开始的时候就发送大量的数据包,也可能会引发其他的问题。

    一般来说计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信是的网络拥堵。在网络出现拥堵时,如果突然发送一个较大量的数据,极可能会导致整个网络的瘫痪。

    TCP引入慢启动机制,先发送少量的数据,探探路,摸清楚当前网络拥堵状态,再决定按照多大的速度传输数据,首先为了在发送端调节所要发送的数据量,定义了一个叫做“拥塞窗口”的概念。于是在慢启动的时候将这个拥塞窗口的大小设置为1个数据段。

    最初发送端的窗口(拥塞窗口)设置为1,。每收到一个确认应答,窗口的值会增加1个段,在发送数据包时,将拥塞窗口的大小和接收端主机的通知的窗口大小作比较,然后按照它们当中的较小的那个值发送比其还要小的数量

    像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动”只是指初始慢,但是增长速度非常快。

    插图:TCP窗口的变化

     

    为了防止增长的这么快,拥堵状况激增甚至导致网络拥塞的发生。引入了慢启动阈值的概念。当拥塞窗口超过这个阈值的时候,不在按照指数增长,而是按照线性方式增长,

    当TCP开始启动的时候,并没有设置相应的慢启动阈值。而是在超时重发的时候,慢启动阈值等于当时拥塞窗口一半的大小

    在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1

    重复确认应答(也就是网络堵塞了,没有滑动窗口内部的一个数据段没有接收到从而引发多次确认应答)而触发的高速重发控制时,慢启动阈值的大小被设置为当前窗口大小的一半(实际已发送但未收到确认应答的数据量),然后将窗口的大小设置为该慢启动阈值+3个数据段的大小。

    当TCP开始通信的以后,网络吞吐量会逐渐上升,但是随着网络拥堵的发生吞吐量也会急速的下降。于是会再次进入吞吐量慢慢上升的过程,因此所谓TCP的吞吐量的特点就好像是在逐步占领网络带宽的感觉。

    (吞吐量是指在没有帧丢失的情况下,设备能够接受的最大速率 )

    少量的丢包我们仅仅是触发超时重传,大量的丢包,我们就认为网路堵塞

    拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大的压力的折中办法

     

    13. 延迟应答

    如果接收数据的主机每次如果都立刻回复确认应答(ACK),可能会返回一个较小的窗口,这是因为刚接收完数据这个时候接收缓存区已满

    假设:接收端缓存区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K;但实际上可能处理端得速度很快,10ms之内就把500K数据从缓存区消费掉了,这种情况下,接收端处理数据还远远没有到达自己的极限,即使这个窗口再放大一些,也能处理过来,如果接收端稍微等一会儿应答,比如等200ms再应答,那么这个时候返回的窗口就是1M

    一定要记得,窗口越大,网络吞吐量越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。

    为此引入一个方法,那就是接收到数据以后并不立即返回应答,而是延迟一端时间的机制

    • 再没有收到2 * 最大段长度的数据为止不做确认应答(根据操作系统的不同,有时也有不论数据大小,只要收到两个包就即可返回确认应答情况)

    • 其他情况下,最大延迟0.5秒发送确认应答(很多操作系统设置为0.2秒左右)

    事实上,不必为每一个数据段都进行一次确认应答,TCP采用滑动窗口的控制机制,因此通常确认应答少一点也无妨。TCP文件传输中,绝大多数是每两个数据段返回一次确认应答

    如图:

    插图:延迟确认应答

    每收到两个数据段发送一次确认应答。不过等待0.2秒后还没有其他数据包到达的情况下才会发送确认应答

     

    14. 捎带应答

    根据应用层协议,发送出去的消息到达对端,对端进行处理以后,会返回一个回执

    在延迟应答的基础上,客户端和服务器在应用层也是“一发一收的”。在此类通信中,TCP的确认应答和回执数据可以通过一个包发送。这种方式叫捎带应答。通过这种机制可以是收发数据量减小

    注意:接收数据以后如果立刻返回确认应答,就无法实现捎带应答。而是将所接受的数据传给应用处理生成数据以后将要回执的数据和确认应答一起发送给对端。

    如果没有启用延迟确认应答就无法实现捎带应答。延迟确认应答是能够提高网络利用率从而降低计算机处理负荷的一种较优的处理机制

    如图:

    插图:捎带应答

     

    15. 面向字节流

    当我们创建一个TCP的socket,同时在内核中创建一个发送缓存区和一个接收缓存区

    • 调用write时,数据会先写到发送缓存区中

    • 如果发送的字节数太长,会被拆分成多个TCP的数据报发出

    • 如果发送的字节数太短了,就会先在缓存区里等待,等到缓存区长度差不多了,或者其他合适的时机发送出去

    • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓存区

    • 然后应用程序可以调用read从接收缓存区拿数据

    • 另一方面,TCP的一个连接,既有发送缓存区,也有接收缓存区,那么对于一个连接,既可以读数据,也可以写数据。这个概念叫做全双工

    由于缓存区的存在,TCP程序的读和写不需要一一匹配

    例如:

    • 写100个字节的数据,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节

    • 读100个字节数据,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次

     

    16. 粘包问题

    粘包问题中的包,是指的是应用层的数据包

    在TCP的协议头中没有如同UDP一样的“报文长度”这样的字段,但是有一个序号这样的字段

    站在传输层的角度,TCP是一个报文一个报文过来的,按照序号排序在缓存区中

    站在应用层的角度,看到的只是一串连续的字节数据

    那么应用程序看到这一连串的字节数据,就不知道从哪里到哪里是一个完成的应用层数据包

    如何解决粘包问题呢?明确两个包之间的边界

    • 对于定长的包,保证每次都按固定大小读取即可

    • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而知道包的结束位置

    • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议是程序员自己来定义的,只要保证分隔符不和正文冲突即可)

    对UDP协议如果还没有给上层交付数据,UDP的报文长度仍然还在。同时UDP是一个一个把数据交付给应用层,这样就有存在明确的数据边界。站在应用层的角度,使用UDP的时候要么收到完整的UDP报文要么不收,不会出现“半个“的情况

     

    17. TCP连接异常情况

    • 进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有区别。机器重启和进程终止一样

    • 机器掉电/网线断开:接收端认为连接还在,一旦接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也在内置了一个保活定时器,,会定期询问对方是否还在。如果对方不在,也会把链接释放。应用层的某些协议, 也有一些这样的检测机制.例如HTTP长连接中, 也会定期检测对方的状态.Q在QQ 断线之后, 也会定期尝试重新连接

     

    18. TCP小结

    18.1 可靠性

    • 检验和

    • 序列号

    • 确认应答

    • 超时重发

    • 延迟重发

    • 连接管理

    • 流量控制

    • 拥塞控制

    18.2 提高性能

    • 滑动窗口

    • 快速重传

    • 延迟应答

    • 捎带应答

    18.3 基于TCP的应用层协议

    • HTTP

    • HTTPS

    • TELNET

    • SSH

    • FTP

    18.4 TCP和UDP的区别

    • TCP:可靠的,面向连接的协议(打电话),传输效率低全双工通信(发送缓存&接受缓存),面向字节流。使用TCP的应用:Web浏览器,电子邮件,文件传输程序

    • UDP:不可靠的,无连接的服务,传输效率高(发送前时延小),面向数据报,尽最大努力交付,无拥塞控制。使用UDP的应用:域名系统(DNS),视频流,IP语音(VoIP)

     

    • 每一条TCP连接只能是点到点的,但是对于UDP来说,支持一对一,一对多,多对一,以及多对多的交互通信

    • 对于TCP来说是可靠的全双工但是对于UDP来说是不可靠的全双工

    • TCP是基于字节流的,看成无结构的字节流进行传输,当应用程序交给TCP的数据长度太长,超过MSS是,TCP就会对数据进行分段,因此TCP的数据是无边界的;而UDP是面向报文的,无论应用程序交给UDP层多长的报文,UDP都不会对数据包进行任何拆分处理,因此UDP保留了应用层数据的边界

    • UDP可以进行广播,但是对于TCP不可以进行广播

     

    • 网络就是生产者和消费者模型

    展开全文
  • 为了简化流程,暂不做三次握手的过程,直接发单个HTTP GET请求的数据包. 思路就是,以太网头 + ip头 + tcp头 + http数据 用telnet测试 away@aways-iMac:~$ telnet baidu.com 80 Trying 123.125.115.110... ...

    为了简化流程,暂不做三次握手的过程,直接发单个HTTP GET请求的数据包.

    思路就是,以太网头 + ip头 + tcp头 + http数据

    用telnet测试

    away@aways-iMac:~$ telnet baidu.com 80
    Trying 123.125.115.110...
    Connected to baidu.com.
    Escape character is '^]'.
    GET / HTTP/1.0
    
    HTTP/1.1 200 OK
    Date: Fri, 04 Jan 2019 01:36:33 GMT
    Server: Apache
    Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
    ETag: "51-47cf7e6ee8400"
    Accept-Ranges: bytes
    Content-Length: 81
    Cache-Control: max-age=86400
    Expires: Sat, 05 Jan 2019 01:36:33 GMT
    Connection: Close
    Content-Type: text/html
    
    <html>
    <meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
    </html>
    Connection closed by foreign host.

    实现代码如下

    // MyTcp.h
    
    #ifndef my_tcp_h
    #define my_tcp_h
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <errno.h>
    #include <sys/socket.h>
    #include <sys/ioctl.h>
    #include <linux/if_packet.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <netinet/tcp.h>
    #include <net/if.h>
    #include <net/ethernet.h>
    #include <arpa/inet.h>
    
    #include <string>
    #include <iostream>
    
    struct psdhdr {
    	uint32_t src_addr;
    	uint32_t dst_addr;
    	uint8_t zero;
    	uint8_t protocol;
    	uint16_t tcp_len;
    };
    
    
    class MyTcp
    {
    public:
    	MyTcp();
    	~MyTcp();
    	void setNic(std::string nic, int src_port);
    	void setDst(std::string dst_ip, int dst_port);
    	int send(std::string data);
    private:
    	int sockfd;
    	struct sockaddr_ll dst_sock_addr;
    	struct ether_header eth;
    	struct iphdr iph;
    	struct tcphdr tcph;
    	struct psdhdr psdh;
    
    	int src_addr;
    	int dst_addr;
    
    	uint16_t getCkSum(uint16_t *addr, int len);
    
    
    };
    
    
    #endif
    //MyTcp.cpp
    
    #include "MyTcp.h"
    
    
    
    
    MyTcp::MyTcp() 
    {
    	sockfd = socket(AF_PACKET, SOCK_RAW, 0);
    	if (sockfd == -1)
    	{
    		perror("socket");
    		exit(1);
    	}
    }
    
    MyTcp::~MyTcp()
    {
    
    }
    
    void MyTcp::setNic(std::string nic, int src_port)
    {
    	struct ifreq if_index, if_mac, if_ip;
    	bzero(&if_index, sizeof(struct ifreq));
    	bzero(&if_mac, sizeof(struct ifreq));
    	bzero(&if_ip, sizeof(struct ifreq));
    	strcpy(if_index.ifr_name, nic.c_str());
    	strcpy(if_mac.ifr_name, nic.c_str());
    	strcpy(if_ip.ifr_name, nic.c_str());
    
    	if (ioctl(sockfd, SIOCGIFINDEX, &if_index) < 0)
    	{
    		perror("siocgifindex");
    		exit(1);
    	}
    	if (ioctl(sockfd, SIOCGIFHWADDR, &if_mac) < 0)
    	{
    		perror("siocgifhwaddr");
    		exit(1);
    	}
    	if (ioctl(sockfd, SIOCGIFADDR, &if_ip) < 0)
    	{
    		perror("siocgifaddr");
    		exit(1);
    	}
    	for (int i = 0; i< 6; i++)
    		eth.ether_shost[i] = if_mac.ifr_hwaddr.sa_data[i];
    	eth.ether_type = htons(ETH_P_IP);
    
    	src_addr = ((struct sockaddr_in *)&if_ip.ifr_addr)->sin_addr.s_addr;
    
    	tcph.source = htons(src_port);
    
    	bzero(&dst_sock_addr, sizeof(dst_sock_addr));
    	dst_sock_addr.sll_ifindex = if_index.ifr_ifindex;
    	dst_sock_addr.sll_halen = ETH_ALEN;
    	dst_sock_addr.sll_protocol = ETH_P_IP;
    }
    
    void MyTcp::setDst(std::string dst_ip, int dst_port) 
    {
    	dst_addr = inet_addr(dst_ip.c_str());
    
    	eth.ether_dhost[0] = 0xd0;
    	eth.ether_dhost[1] = 0x17;
    	eth.ether_dhost[2] = 0xc2;
    	eth.ether_dhost[3] = 0xd3;
    	eth.ether_dhost[4] = 0xb5;
    	eth.ether_dhost[5] = 0xc9;
    
    	iph.ihl = 5;
    	iph.version = 4;
    	iph.tos = IPTOS_LOWDELAY;
    	iph.id = htons(54321);
    	iph.ttl = 64;
    	iph.protocol = IPPROTO_TCP;
    	iph.saddr = src_addr;
    	iph.daddr = dst_addr;
    	iph.tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr));
    	iph.check = 0;
    
    	tcph.dest = htons(dst_port);
    	tcph.seq = 0x0;
    	tcph.ack_seq = 0x0;
    	tcph.doff = 5;
    	tcph.res1 = 0;
    	tcph.urg = 0;
    	tcph.ack = 0;
    	tcph.psh = 1;
    	tcph.rst = 0;
    	tcph.syn = 0;
    	tcph.fin = 0;
    	tcph.window = htons(155);
    	tcph.check = 0;
    	tcph.urg_ptr = 0;
    
    	psdh.src_addr = src_addr;
    	psdh.dst_addr = dst_addr;
    	psdh.zero = 0;
    	psdh.protocol = IPPROTO_TCP;
    	// psdh.tcp_len = htons(sizeof(struct tcphdr));
    }
    
    int MyTcp::send(std::string data) 
    {
    	int packet_len, psd_packet_len;
    	char * packet, * psd_packet;
    
    	packet_len = sizeof(struct ether_header) + sizeof(struct iphdr) + sizeof(struct tcphdr) + data.size();
    	psd_packet_len = sizeof(struct psdhdr) + sizeof(struct tcphdr) + data.size();
    	packet = (char *)malloc(packet_len);
    	if (packet == NULL)
    	{
    		perror("out of memory");
    		exit(1);
    	}
    	psd_packet = (char *)malloc(psd_packet_len);
    	if (psd_packet == NULL)
    	{
    		free(packet);
    		perror("out of memory");
    		exit(1);
    	}
    	iph.tot_len = htons(packet_len - sizeof(eth));
    	iph.check = getCkSum((uint16_t*)&iph, sizeof(iphdr));  // /2?
    
    	psdh.tcp_len = htons(sizeof(tcph) + data.size());
    	memset(psd_packet, 0, psd_packet_len);
    	memcpy(psd_packet, (char *)&psdh, sizeof(psdh));
    	memcpy(psd_packet + sizeof(psdh), (char *)&tcph, sizeof(tcph));
    	if (!data.empty())
    	{
    		memcpy(psd_packet + sizeof(psdh) + sizeof(tcph), data.c_str(), data.size());
    	}
    	tcph.check = getCkSum((uint16_t *)psd_packet, psd_packet_len);
    
    	std::cout << "tcp cksum: " << tcph.check <<std::endl;
    
    
    	memset(packet, 0, packet_len);
    	memcpy(packet, (char *)&eth, sizeof(eth));
    	memcpy(packet + sizeof(eth), (char *)&iph, sizeof(iph));
    	memcpy(packet + sizeof(eth) + sizeof(iph), (char *)&tcph, sizeof(tcph));
    	if (!data.empty())
    	{
    		memcpy(packet + packet_len - data.size(), data.c_str(), data.size());	
    	}
    
    	if (sendto(sockfd, packet, packet_len, 0, (struct sockaddr *)&dst_sock_addr,sizeof(dst_sock_addr)) < 0)
    	{
    		// perror("send failed");
    		std::cout << "send failed\n";
    	}
    
    	free(packet);
    	free(psd_packet);
    	return 0;
    }
    
    
    
    uint16_t MyTcp::getCkSum(uint16_t *addr, int len)
    {
    	int				nleft = len;
    	uint32_t		sum = 0;
    	uint16_t		*w = addr;
    	uint16_t		answer = 0;
    	
    	while (nleft > 1)  {
    		sum += *w++;
    		nleft -= 2;
    	}
    	
    	if (nleft == 1) {
    		*(unsigned char *)(&answer) = *(unsigned char *)w ;
    		sum += answer;
    	}
    	
    	sum = (sum >> 16) + (sum & 0xffff);
    	sum += (sum >> 16);
    	answer = ~sum;
    	return(answer);
    }
    // main.cpp
    
    #include "MyTcp.h"
    
    
    int main()
    {
    	MyTcp mytcp;
    	mytcp.setNic("ens33", 75737);
    	mytcp.setDst("192.168.88.222", 80);
    	mytcp.send("GET / HTTP/1.0\r\n\r\n");
    }

    抓包结果如下:

    tcpdump -i ens33 host 192.168.88.222 and host 192.168.88.223 -e -v and tcp 

     

    11:07:29.909750 00:0c:29:a2:2f:82 (oui Unknown) > d0:17:c2:d3:b5:c9 (oui Unknown), ethertype IPv4 (0x0800), length 72: (tos 0x10, ttl 64, id 54321, offset 0, flags [none], proto TCP (6), length 58)
        192.168.88.223.10201 > 192.168.88.222.http: Flags [P], cksum 0x7558 (correct), seq 0:18, win 155, length 18: HTTP, length: 18
        GET / HTTP/1.0

    展开全文
  • Linux发送HTTP协议请求

    万次阅读 2016-06-06 22:36:05
    Linux系统中用C语言实现的HTTP协议的POST和GET请求,下面是代码实现,如果要测试需要搭建个后台服务器的环境, 作者测试用的是PHP开发环境,具体搭建可参看另一篇文章:...

          在Linux系统中用C语言实现的HTTP协议的POST和GET请求,下面是代码实现,如果要测试需要搭建个后台服务器的环境,

    作者测试用的是PHP开发环境,具体搭建可参看另一篇文章:http://blog.csdn.net/hanbo622/article/details/51598648

    demo.c

    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    int main(int argc, char *argv[])
    {
    	unsigned short port = 80;           // 服务器的端口号
    	char *server_ip = "192.168.2.203";  // 服务器ip地址
    
    	int sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建TCP套接字
    	if(sockfd < 0)
    	{
    		perror("socket");
    		exit(-1);
    	}
    	
    	struct sockaddr_in server_addr; //定义服务器信息结构体
    	bzero(&server_addr,sizeof(server_addr)); 
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_port = htons(port);
    	inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
    	
    	int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));   // 主动连接服务器
    	if(err_log != 0)
    	{
    		perror("connect");
    		close(sockfd);
    		exit(-1);
    	}
    	
    	char str1[1024] = "";
    	char data[]="username=hanbo&num=123"; //POST 数据
    #if 1    //POST 格式请求
    	sprintf(str1, "%s\r\n","POST http://192.168.2.203/index.php HTTP/1.1"); //http://192.168.2.203/index.php 服务端接收数据处理文件地址
    	sprintf(str1, "%s%s\r\n",str1,"Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, application/vnd.ms-powerpoint, application/vnd.ms-excel, */*");
    	sprintf(str1, "%s%s\r\n",str1,"Referer: */*");
    	sprintf(str1, "%s%s\r\n",str1,"Accept-Language: en-US,zh-CN;q=0.5");
    	sprintf(str1, "%s%s\r\n",str1,"User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; qdesk 2.4.1265.203; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3)");
    	sprintf(str1, "%s%s\r\n",str1,"Content-Type: application/x-www-form-urlencoded");
    	sprintf(str1, "%s%s\r\n",str1,"Accept-Encoding: gzip, deflate");
    	sprintf(str1, "%s%s\r\n",str1,"Host: 192.168.2.203"); //服务器地址
    	sprintf(str1, "%s%s%d\r\n",str1,"Content-Length: ",strlen(data)); //数据长度
    	sprintf(str1, "%s%s\r\n",str1,"Connection: Keep-Alive");
    	sprintf(str1,"%s%s\r\n",str1,"Cache-Control: no-cache");
    	sprintf(str1, "%s\r\n",str1); //多加个回车
    	sprintf(str1, "%s%s\r\n",str1,data);
    #else	  //GET 格式请求
    	sprintf(str1, "%s\r\n","GET http://192.168.2.203/index.php?username=hanbo&num=123 HTTP/1.1"); //服务端接收数据处理文件地址,并带参数
    	sprintf(str1, "%s%s\r\n",str1,"Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, application/vnd.ms-powerpoint, application/vnd.ms-excel, */*");
    	sprintf(str1, "%s%s\r\n",str1,"Accept-Language: en-US,zh-CN;q=0.5");
    	sprintf(str1, "%s%s\r\n",str1,"User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; qdesk 2.4.1265.203; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3)");
    	sprintf(str1, "%s%s\r\n",str1,"Accept-Encoding: gzip, deflate");
    	sprintf(str1, "%s%s\r\n",str1,"Host: 192.168.2.203"); //服务器地址
    	sprintf(str1, "%s%s\r\n",str1,"Connection: Keep-Alive");
    	sprintf(str1,"%s%s\r\n",str1,"Cookie: JSESSIONID=5386A9443729D7EB0B61E38A9C7CF52F");
    	sprintf(str1, "%s\r\n",str1);	
    #endif
    	printf("----------------------------- HTTP Data ----------------------------------\n");
    	printf("%s",str1);
    	printf("--------------------------- Data Len=%d ----------------------------------\n\n",strlen(str1));
    	
    	int ret=send(sockfd, str1, strlen(str1), 0);   // 向服务器发送信息
    	if(ret<0)
    	{
    		perror("send");
    		close(sockfd);
    		exit(-1);
    	}
    	
    	char recv_buf[521]="";
    	recv(sockfd, recv_buf, sizeof(recv_buf), 0);
    	printf("------------------------ server retrun data -------------------------------\n");
    	printf("%s\n\n",recv_buf);
    	close(sockfd);
    
    	return 0;
    }


    在搭建好的服务端,在Apache安装目录D:\AppServ\Apache24\htdocs 中新建文件index.php内容如下:

    <?php
    /* 接收POST 格式数据处理 */
    echo "username=".$_POST['username'];
    echo "\nnum=".$_POST['num'];
    
    /* 接收GET 格式数据处理 */
    // echo "username=".$_GET['username'];
    // echo "\nnum=".$_GET['num'];
    ?>

    运行POST请求会出现如下结果:

    ----------------------------- HTTP Data ----------------------------------
    POST http://192.168.2.203/index.php HTTP/1.1
    Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, application/vnd.ms-powerpoint, application/vnd.ms-excel, */*
    Referer: */*
    Accept-Language: en-US,zh-CN;q=0.5
    User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; qdesk 2.4.1265.203; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3)
    Content-Type: application/x-www-form-urlencoded
    Accept-Encoding: gzip, deflate
    Host: 192.168.2.203
    Content-Length: 22
    Connection: Keep-Alive
    Cache-Control: no-cache
    
    username=hanbo&num=123
    --------------------------- Data Len=701 ----------------------------------
    
    ------------------------ server retrun data -------------------------------
    HTTP/1.1 200 OK
    Date: Mon, 06 Jun 2016 02:40:28 GMT
    Server: Apache/2.4.10 (Win32) PHP/5.5.25
    X-Powered-By: PHP/5.5.25
    Content-Length: 22
    Keep-Alive: timeout=5, max=100
    Connection: Keep-Alive
    Content-Type: text/html
    
    username=hanbo
    num=123
    
    


    运行GET请求会出现如下结果:

    ----------------------------- HTTP Data ----------------------------------
    GET http://192.168.2.203/index.php?username=hanbo&num=123 HTTP/1.1
    Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, application/vnd.ms-powerpoint, application/vnd.ms-excel, */*
    Accept-Language: en-US,zh-CN;q=0.5
    User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; qdesk 2.4.1265.203; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.3)
    Accept-Encoding: gzip, deflate
    Host: 192.168.2.203
    Connection: Keep-Alive
    Cookie: JSESSIONID=5386A9443729D7EB0B61E38A9C7CF52F
    
    --------------------------- Data Len=644 ----------------------------------
    
    ------------------------ server retrun data -------------------------------
    HTTP/1.1 200 OK
    Date: Mon, 06 Jun 2016 02:42:52 GMT
    Server: Apache/2.4.10 (Win32) PHP/5.5.25
    X-Powered-By: PHP/5.5.25
    Content-Length: 22
    Keep-Alive: timeout=5, max=100
    Connection: Keep-Alive
    Content-Type: text/html
    
    username=hanbo
    num=123
    


     


     

    展开全文
  • 1. 初始化 static int __init inet_init(void){ /*...*/ /* Register the socket-side information for inet_create. */ for (r = &inetsw[0]; r < &inetsw[SOCK_MAX];... INIT_LIST_HEAD(r);...
  • Linux下进行TCP简单通信

    千次阅读 2018-05-14 20:18:39
    体会TCP与UDP编程的不同,UDP编程:http://blog.csdn.net/yueguanghaidao/article/details/7055985二、实验平台Linux操作系统三、实验内容编写LinuxTCP服务器套接字程序,程序运行时服务器等待客户的连接,一旦...
  • netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' 会得到类似下面的结果,具体数字会有所不同: LAST_ACK 1 SYN_RECV 14 ESTABLISHED 79 FIN_WAIT1 28 ...
  • Linux tcpdump命令详解

    千次阅读 2018-09-01 14:55:40
    简介 用简单的话来定义tcpdump,就是:dump the traffic on a network,根据使用者的定义对网络上的数据包进行截获的包分析工具。 tcpdump可以将网络中传送的数据包的“头”完全截获下来提供分析。...
  • Linux-TCP协议

    2018-07-04 22:25:00
    TCP协议是传输层中使用最为广泛的一协议,它可以向上层提供面向连接的协议,使上层启动应用程序,以确保网络上所发送的数据报被完整接收。就这种作用而言,TCP 的作用是提供可靠通信的有效报文协议。一旦数据报被...
  • Linux查看TCP连接情况

    万次阅读 2019-06-17 16:03:35
    netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state) print key,"\t",state[key]}' 会得到类似下面的结果,具体数字会有所不同: TIME_WAIT 5856 CLOSE_WAIT 268 FIN_WAIT1 3 ESTABLISHED ...
  • Linux 监控tcp连接数及状态

    千次阅读 2019-01-10 16:43:54
    二、查看TCP连接数 查看tcp连接数状态    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'  统计8080端口上有多少个TCP连接,命令:    netstat -ant |grep 80|wc -l  TCP...
  • Linux TCP调试利器 - nc

    万次阅读 2015-06-29 15:30:00
    比如你想给某一个endpoint发送轻松 nc
  • linux TCP 参数设置

    千次阅读 2013-08-26 10:39:45
    此文为网络转载,对理解linux内核tcp参数设置有一定帮助,设置tcp参数一定要小心谨慎,轻易不要更改线上环境,我贴一下我们线上环境中,sysctl.conf的内容,见文章底部 net.ipv4.tcp_tw_reuse = 1 ...
  • Linux C Socket TCP编程介绍及实例

    万次阅读 多人点赞 2016-11-25 15:49:43
    1、TCP网络编程主要流程 图1.1 注意:图1.1中可以看到close指向read并且标有结束连接的指示,可能有些人会有疑问,这个标注的意思是服务器在处理客户端的时候是循环读取的,如果客户端没有发送数据服务器处理...
  • LinuxTCP的keepalive机制

    千次阅读 2018-08-12 23:16:10
    LinuxTCP的keepalive机制及主要参数
  • Linux_Linux Shell 用curl 发送请求

    万次阅读 2017-04-06 22:04:39
    linux curl是通过url语法在命令行下上传或下载文件的工具软件,它支持http,https,ftp,ftps,telnet等...一、Linux curl用法举例: 1. linux curl抓取网页: 抓取百度:     1 curl http://www.baidu.com
  • 一 超时重传 ...TCP协议为TCP报文制定了一个定时器,它用于在给定的时间内接收到对端传回来的确认报文,加入超过给定时间确认报文段还没有传回到发送端,这时发送端就会重新发送上次发送TCP数据包,并且延
  • 基于Linux下的TCP编程

    万次阅读 多人点赞 2013-07-14 15:11:26
    基于LinuxTCP网络编程一.LinuxTCP编程框架TCP网络编程的流程包含服务器和客户端两种模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则...
  • Linux TCP/IP 协议栈源码分析 - 数据 发送/接收 流程图
  • linux发送arp请求报文 代码

    千次阅读 2011-09-29 16:29:56
    1,本代码主要是参考 宋敬彬的linux 网络编程>写的,如果完全按照书上的代码编译不能通过,编译通过后运行出现提示:Transport endpoint is ...用tcpdump抓包发现没有arp请求报文发送出去。我用的操作系统 为ubuntu9.10
1 2 3 4 5 ... 20
收藏数 114,569
精华内容 45,827
关键字:

linux发送tcp请求