TCP回射服务器/客户端分析

本文将对一个简单的TCP回射服务器和客户端进行抓包,从而分析一次成功而理想TCP会话的基本流程,多次不成功或与预期不一致的抓包结果将在下篇博文进行分析

本文程序编译环境为:

Linux version 3.16.4-1-ARCH

gcc version 4.9.1 20140903 (prerelease)

Glibc 2.18

服务器代码如下:

 1 #include <unistd.h>
 2 #include <sys/types.h>
 3 #include <sys/socket.h>
 4 #include <arpa/inet.h>
 5 #include <netinet/in.h>
 6
 7 #include <stdio.h>
 8 #include <stdlib.h>
 9 #include <string.h>
10
11 #define ERR_EXIT(exp) 12     do13     {14         perror(exp);15         exit(EXIT_FAILURE);16     }while(0)
17
18 #define BUFSIZE 1024
19
20 int main(int argc, char *argv[])
21 {
22     if(argc != 2)
23         ERR_EXIT("Usage: a.out <port>");
24
25     int server_sock;
26     int client_sock;
27     struct sockaddr_in server_addr;
28     struct sockaddr_in client_addr;
29     socklen_t server_len;
30     socklen_t client_len;
31
32     server_sock = socket(PF_INET, SOCK_STREAM, 0);
33     if(server_sock == -1)
34         ERR_EXIT("socket");
35
36     memset(&server_addr, 0, sizeof(server_addr));
37     server_addr.sin_family = AF_INET;
38     server_addr.sin_addr.s_addr = INADDR_ANY;
39     server_addr.sin_port = htons(atoi(argv[1]));
40     server_len = sizeof(server_addr);
41     client_len = sizeof(client_addr);
42
43     if(bind(server_sock, (struct sockaddr*)&server_addr, server_len) == -1)
44         ERR_EXIT("bind");
45
46     if(listen(server_sock, 5) == -1)
47         ERR_EXIT("listen");
48
49     client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
50     if(client_sock == -1)
51         ERR_EXIT("accept");
52     char buffer[BUFSIZE];
53     read(client_sock, buffer, BUFSIZE);
54     write(client_sock, buffer, strlen(buffer));
55
56     sleep(3);
57     close(client_sock);
58     close(server_sock);
59     return 0;
60 }

客户端代码如下:

 1 #include <unistd.h>
 2 #include <sys/types.h>
 3 #include <sys/socket.h>
 4 #include <arpa/inet.h>
 5 #include <netinet/in.h>
 6
 7 #include <stdio.h>
 8 #include <stdlib.h>
 9 #include <string.h>
10
11 #define ERR_EXIT(exp) 12     do13     {14         perror(exp);15         exit(EXIT_FAILURE);16     }while(0)
17
18 #define BUFSIZE 1024
19
20 int main(int argc, char *argv[])
21 {
22     if(argc != 3)
23         ERR_EXIT("Usage: a.out <server_ip> <port>");
24
25     int server_sock;
26     struct sockaddr_in server_addr;
27     socklen_t server_len;
28
29     server_sock = socket(PF_INET, SOCK_STREAM, 0);
30     if(server_sock == -1)
31         ERR_EXIT("socket");
32
33     server_addr.sin_family = AF_INET;
34     server_addr.sin_addr.s_addr = inet_addr(argv[1]);
35     server_addr.sin_port = htons(atoi(argv[2]));
36     server_len = sizeof(server_addr);
37
38     if(connect(server_sock, (struct sockaddr*)&server_addr, server_len) == -1)
39         ERR_EXIT("connect");
40
41     char buffer[BUFSIZE] = "Hello World!";
42     write(server_sock, buffer, strlen(buffer));
43     memset(buffer, 0, BUFSIZE);
44     read(server_sock, buffer, BUFSIZE);
45     printf("%s\n", buffer);
46
47     sleep(1);
48     close(server_sock);
49     return 0;
50 }

首先展示抓包结果,这是一个完全符合书本理论的理想状况,下篇博客将会通过调试器等手段制造非理想状况,进而分析其行为,因此不对TCP包结构进行讨论,TCP包的WIN字段用于滑动窗口的控制,本文暂不涉及

首先:一次TCP会话的基本流程应该分为以下三步

  1. 与对方套接字建立连接
  2. 与对方套接字进行数据交换
  3. 断开与对方套接字的连接

下面即对抓包结果进行分析,从而分析出这三个步骤中实际发生了什么,需要说明的是,抓包得到的No序号和TCP包中seq和ack的序号不是一个东西,由于我使用SSH连接了测试机进行控制,因此抓包软件连同SSH的数据包一并抓取,造成与我们测试内容相关的数据包序号不连续,不过这并不影响我们的分析和理解

  • 22号数据包,客户端发往服务端,SYN置位,表面消息类型为SYN,即同步消息,Seq=0,这是客户端首次请求连接时所产生的消息

    • seq的含义为“客户端发往服务端的seq为0,长度为0的数据包,等待服务端确认收到,并请求客户端发往服务端发送seq为1的数据包”
  • 24号数据包,服务器收到了连接请求SYN,做出应答,应答消息的数据包中,SYN,ACK置位,即消息类型为SYN+ACK,Seq=0,Ack=
    • Seq含义为“服务端将向客户端发送seq为0长度为0的数据包,等待客户端确认收到,并请求发送服务器发往客户端的seq为1的数据包”
    • Ack的含义为“服务端已收到客户端发往服务端的seq为0长度为0的数据包,客户端可以发送seq为1的数据包”
    • 需要注意的是,在上文中我采用了“从a发往b的seq为n的长度为k数据包”,非常啰嗦,其原因在于

      • seq与ack值的增长并非是按照数据包数量增长的,其增量为传输的数据字节数,即LEN,其增长规则为:ack = seq + len + 1,此式与后面结果有所冲突,待有更深入理解后再行更正或解释
      • 与书本中的描述不同,书本描述中SYN包的seq和SYN+ACK包的seq不相同,造成一种“客户端套接字和服务端套接字发数据包seq是在一个线性空间中”的错觉,此处抓包结果已经展示出,客户端的seq和服务端的seq是独立位于两个线性空间中,互不干涉,没有查看TCP协议的标准文档,此处暂时存疑
  • 25号数据包,客户端收到了服务端发来的SYN+ACK消息,做出ACK应答,seq为1,ack为1,含义可以参照上文,

22,24,25号数据包即TCP会话基本流程的第一步:与对方套接字建立连接,这个过程一共进行了三次数据包的传递,也就是俗称的三次握手

  • 26号数据包由客户端发往服务端,PSH,ACK置位,seq=1,ack=1,len=12
  • 27号数据包由服务端发往客户端,ACK置位,seq=1,ack=13,可以看出,服务端收到了客户端发来的seq为1,长度为12的数据包,并通知客户端可以发送seq为13的数据包,从而证实了,数据包seq的增量取决于传输的字节数
  • 28号数据包由服务端发往客户端,即回射的数据,PSH,ACK置位,seq=1,ack=13,Len=13,
    • 从这个包可以得出两点

      • 在没有收到请求的数据包时,tcp会话的某一方会重新请求该数据包
      • tcp会话的某一方不会做出“某个数据包已经请求,以后将会收到”的假设
    • 回顾之前的包,不难发现,所有的tcp包中都有seq和ack两个值,因此可以猜想,seq和ack是面向连接的TCP会话可靠性的重要保证和依赖,暂时没有查询标准文档,因此这儿仅作猜想
    • 这个数据包的LEN=13,客户端发来的数据包LEN确为12,这是一个很奇怪的现象,查看26和28号包的data段,可以发现其中差异
      • 26号:48:65:6c:6c:6f:20:57:6f:72:6c:64:21
      • 28号:48:65:6c:6c:6f:20:57:6f:72:6c:64:21:04
      • 多出了最后一个04,ASCII表示为EOT(End of Transmission),此处暂时存疑,以后填坑
  • 29号包,由客户端发往服务端,ACK置位,seq=13,ack=14,表明客户端成功接收了服务端回射的数据

26,27,28,29四个数据包展示了客户端服务端进行一次数据交换的过程,两个来回,并根据实际的业务逻辑而变化,这就是TCP会话中的第二步:与对方套接字进行数据交换

  • 32号包,客户端发往服务端,FIN,ACK置位,表明即将断开连接,等待服务端回应
  • 34号包,服务端发往客户端,ACK置位,对客户端的FIN请求做出了回应
  • 36号包,服务端发往客户端,FIN,ACK置位,表明服务端也将断开和客户端的连接,等待客户端回应
  • 37号包,客户端发往服务端,ACK置位,对服务端的断开请求进行回应,并真正断开(这么说并不确切,不过本文不进行深究)

32,34,36,37号包实现了一次TCP会话中的第三步:断开与对方套接字的连接,这个过程经历了四次数据包的传递,也就是俗称的四次挥手,至此,一次TCP会话成功完成,下篇博文将会讨论一次TCP会话中的状态转移,并分析一些和本文不同的抓包结果

时间: 2024-10-19 03:38:37

TCP回射服务器/客户端分析的相关文章

TCP回射服务器修订版(ubuntu 18.04)

一.需求 把https://www.cnblogs.com/soldierback/p/10673345.html中的TCP回射服务器程序重写成使用select来处理任意个客户的单进程 程序,而不是为每个进程派生一个子进程 二.分析 (1)服务器有单个监听描述符    (2)服务器只维护一个读描述符集:假设服务器是在前台启动的,那么描述符0.1.2将分别被设置为标准输入.标准输出和标准错误输出:可见监听   套接字的第一个可用描述符是3    (3)服务器维护一个名为clients的整型数组,它

TCP回射服务器程序:main函数

TCP回射并发服务器 1.创建套接字,绑定服务器的众所周知端口 创建一个TCP套接字,在待绑定到该TCP套接字的网际网套接字地址结构中填入通配地址(INADDR_ANY) 和服务器的众所知周(SERV_PORT,在头文件中unp.h中其定义为9877) 绑定通配地址是在告知系统: 要是系统是多宿主机,我们将接受目的地地址为任何本地接口的连接 我们对TCP端口号的选择应该比1023大,比5000大,比49152小,而且不和任何注册的端口冲突 listen把该套接字地址转换成一个监听套接字 2.等待

TCP回射服务器程序:str_echo函数

str_echo函数执行处理每个客户的服务: 从客户读入数据,并把它们回射给客户 读入缓冲区并回射其中内容: read函数从套接字读入数据,writen函数把其中内容回射给客户 如果客户关闭连接,那么接收到客户的FIN将导致服务器子进程的read函数返回0,这又导致str_echo函数的返回,从而终止子进程 #include "unp.h" void str_echo(int sockfd) { ssize_t n; char buf[MAXLINE]; again: while (

Linux非阻塞IO(五)使用poll实现非阻塞的回射服务器客户端

前面几节我们讨论了非阻塞IO的基本概念.Buffer的设计以及非阻塞connect的实现,现在我们使用它们来完成客户端的编写. 我们在http://www.cnblogs.com/inevermore/p/4049165.html中提出过,客户端需要监听stdin.stdout和sockfd. 这里需要注意的是 只有缓冲区可写的时候,才去监听sockfd和stdin的读事件. 过去在阻塞IO中,我们总是监听sockfd的读事件,因为每当sockfd可读,我们就去调用用户的回调函数处理read事件

UNIX网络编程卷1 回射服务器程序 TCP服务器程序设计范式 四个版本

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 这是一个简单的回射服务器程序.它将客户发送的数据读入缓冲区并回射其中内容 下面我会介绍同一个使用 TCP 协议的回射服务器程序的几个不同版本,分别是 fork 版本.select 版本.poll 版本.多线程版本 fork 版本:为每一个客户连接派生(fork) 一个子进程用来处理客户请求 /** * TCP/IPv4 协议相关 * **/ #include "unp.h" in

第九章 TCP和UDP同时用复用一个端口实现一个回射服务器

#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

System V实现的一个消息回射服务器与客户端

echocli.c #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #define ERR_EXIT(m) \ do \ { \ per

第十二篇:并发回射服务器的具体实现及其中僵尸子进程的清理( 上 )

前言 本文将分为两个部分,第一部分具体实现一对并发回射服务器/客户程序( 看过前面那篇文章的这部分可不看 重复了 ):第二部分为服务器添加僵尸子进程自动清理机制. 那么服务器具体怎么实现并发?怎么会有僵尸进程?僵尸进程又是什么?如何处理这些僵尸进程 ... 本文将为你一一解惑. 回射并发服务器 功能:接收用户发送过来的数据后再发送回用户,且能同时处理多个用户请求. 大体思路:每当收到用户请求,服务器就fork一个子进程,让子进程去处理客户请求. 实现代码: 1 #include "unp.h&q

socket编程之并发回射服务器3

在socket编程之并发回射服务器一文中,服务器采用多进程的方式实现并发,本文采用多线程的方式实现并发. 多线程相关API: // Compile and link with -pthread int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); int pthread_join(pthread_t thread, void **