网络通信之socket工作原理

一、进程通信

  • 网间进程通信 解决不同主机进程间的相互通信问题
  • TCP/IP协议族中,网络层的 ip地址”可以唯一标识网络中的主机,而传输层的 协议+端口”可以唯一标识主机中的应用程序(进程)。利用三元组(ip地址,协议,端口)就可以标识网络的进程
  • 几乎所有的应用程序都是采用socket实现网络进程之间的通信。

二、什么是TCP/IP、UDP?

  • TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议。
  • TCP/IP协议族包括运输层、网络层、链路层。
  • UDP(User Data Protocol,用户数据报协议)与TCP相对应的协议。属于TCP/IP协议族中的一种。
  • UDP属于传输层
  • UDP是面向非连接的协议,它不与对方建立连接,而是直接把要发的数据报发给对方。
  • UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景
  • UDP无需建立类如三次握手的连接,通信效率高

image

三、Socket是什么?

  • Socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。
    image
  • 套接字描述符是一个整数,与句柄相似。常用句柄中,0是标准输入,1是标准输出,2是标准错误输出。对应的FILE * 结构的表示就是stdin、stdout、stderr
  • 当应用程序要为因特网通信创建一个套接字(socket)时,操作系统返回一个小整数作为描述符(descriptor)来标识这个套接字。应用程序以该描述符作为传递参数,通过调用函数来完成某种操作
  • 应用程序使用描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。
    image
  • 系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。

针对套接字的系统数据结构:

  1. 创建一个套接字:socket函数。单个系统调用可以创建任何套接字。一旦套接字创建后,应用程序需要调用其他函数来指定具体细节
  2. 套接字的内部数据结构包含很多字段,应用程序创建套接字后,在该套接字可以使用之前,必须调用其他的过程来填充这些字段
    文件描述符和文件指针的区别:
  • 文件描述符:在linux系统中打开文件获得文件描述符,是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针

  • 文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄

基本的SOCKET接口函数

  • 服务器端初始化Socket,与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。如果客户端初始化Socket,连接服务器(connect),连接成功,客户端与服务器端的连接建立。客户端发送数据请求,服务器端接收请求并处理请求,并把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
    image

socket()函数

int  socket(int protofamily, int type, int protocol);//返回sockfd
sockfd是描述符

bind()函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

listen()、connect()函数

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr

accept()函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd

read()、write()等函数

网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

#include <unistd.h>
 
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
 
#include <sys/types.h>
#include <sys/socket.h>
 
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
 
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

close()函数

#include <unistd.h>
int close(int fd);

四、Socket TCP

4.1 Socket中TCP的建立(三次握手)

第一次握手(客户端发送):建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认(connect进入阻塞状态);SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手(服务器发送):服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态(accept进入阻塞状态);

第三次握手(客户端发送):客户端收到服务器的SYN+ACK包(客户端connect返回,并对SYN K进行确认),向服务器发送确认包ACK(ack=k+1), 此包发送完毕(服务器收到ACK K+1时,accept返回),客户端和服务器进入ESTABLISHED状态,完成三次握手。

一个完整的三次握手就是: 客户端A发送Syn包请求---服务器B应答ACK并发送Syn(ACK+Syn)---客户端A应答ACK再次确认
image

4.2 TCP连接的终止(四次握手释放)

  • TCP连接是全双工的,每个方向都必须单独进行关闭。
  • 原则是当一方完成它的数据发送任务后就能 发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
    image
    接口函数:
    image
  1. 客户端A发送一个FIN,来关闭客户A到服务器B的数据传送(报文段4)。

  2. 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。(服务器B不能收了,但仍可以发)

  3. 服务器B关闭与客户端A的连接发送一个FIN给客户端A(报文段6)。

  4. 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

一个完整的四次握手是:客户端A发送Fin--服务器B回复ACK---服务器B发送FIN---客户端A回复ACK

  • 客户端A某应用进程调用close主动关闭连接,TCP发送一个FIN
  • 服务器B接收到FIN,执行被动关闭,对FIN进行确认。它的接收是文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据
  • 一段时间后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
  • 接收到这个FIN的源发送端TCP对它进行确认。
    每个方向上都有一个FIN和ACK。

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

4.3 Socket TCP实例

4.3.1 Server.c

服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。

/* File Name: server.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#define DEFAULT_PORT 8000
#define MAXLINE 4096
int main(int argc, char** argv)
{
 int    socket_fd, connect_fd;
 struct sockaddr_in servaddr;
 char    buff[4096];
 int     n;
 //初始化Socket
 if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){
 printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
 exit(0);
    }
 //初始化
 memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT
 
 //将本地地址绑定到所创建的套接字上
 if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
 printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
 exit(0);
    }
 //开始监听是否有客户端连接
 if( listen(socket_fd, 10) == -1){
 printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
 exit(0);
    }
 printf("======waiting for client's request======\n");
 while(1){
//阻塞直到有客户端连接,不然多浪费CPU资源。
 if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){
 printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
 continue;
    }
//接受客户端传过来的数据
    n = recv(connect_fd, buff, MAXLINE, 0);
//向客户端发送回应数据
 if(!fork()){ /*紫禁城*/
 if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)
        perror("send error");
        close(connect_fd);
 exit(0);
    }
    buff[n] = '\0';
 printf("recv msg from client: %s\n", buff);
    close(connect_fd);
    }
    close(socket_fd);
}

4.3.2 client.C

/* File Name: client.c */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
 
#define MAXLINE 4096
 
int main(int argc, char** argv)
{
 int    sockfd, n,rec_len;
 char    recvline[4096], sendline[4096];
 char    buf[MAXLINE];
 struct sockaddr_in servaddr;
 
 
 if( argc != 2){
 printf("usage: ./client <ipaddress>\n");
 exit(0);
    }
 
 
 if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
 printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
 exit(0);
    }
 
 memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(8000);
 if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
 printf("inet_pton error for %s\n",argv[1]);
 exit(0);
    }
 
 
 if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
 printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
 exit(0);
    }
 
 printf("send msg to server: \n");
    fgets(sendline, 4096, stdin);
 if( send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
 printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
 exit(0);
    }
 if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {
       perror("recv error");
 exit(1);
    }
    buf[rec_len]  = '\0';
 printf("Received : %s ",buf);
    close(sockfd);
 exit(0);
}

头文件types.h找不到。使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,使用apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下

五、Socket UDP

5.1 UDP编程框架

image

  • 客户端要发起一次请求,仅需两个步骤(socket和sendto)
  • 服务器端接受客户端消息,仅需三个步骤(socket、bind、recvfrom)

5.2 UDP程序设计常用函数

5.2.1 socket函数

#include <sys/types.h>    
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain:用于设置网络通信的域
Name Purpose
AF_UNIX, AF_LOCAL Local communication
AF_INET IPv4 Internet protocols //用于IPV4
AF_INET6 IPv6 Internet protocols //用于IPV6
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device
AF_X25 ITU-T X.25 / ISO-8208 protocol
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK AppleTalk
AF_PACKET Low level packet interface
AF_ALG Interface to kernel crypto API

对于该参数仅需熟记AF_INET和AF_INET6

5.2.2 sendto函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

sockfd:正在监听端口的套接口文件描述符,通过socket获得
buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
len:发送缓冲区的大小,单位是字节
flags:填0即可
dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
addrlen:表示第五个参数所指向内容的长度
返回值:成功:返回发送成功的数据长度
失败: -1

5.2.3 recvfrom函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

sockfd:正在监听端口的套接口文件描述符,通过socket获得
buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
len:接收缓冲区的大小,单位是字节
flags:填0即可
src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
addrlen:表示第五个参数所指向内容的长度
返回值:成功:返回接收成功的数据长度
失败: -1

5.2.4 bind函数

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

sockfd:正在监听端口的套接口文件描述符,通过socket获得
my_addr:需要绑定的IP和端口
addrlen:my_addr的结构体的大小
返回值:成功:0
失败:-1

5.2.5 close函数

#include <unistd.h>
int close(int fd);

5.3 UDP通用框架搭建

用于一台主机不同端口的UDP通信.
不同主机通信:

  • client的代码的第49行的注释打开,并注释掉下面那行,在宏定义里填入通信的server ip

5.3.1 Server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 1024

void handle_udp_msg(int fd)
{
    char buf[BUFF_LEN];  //接收缓冲区,1024字节
    socklen_t len;
    int count;
    struct sockaddr_in clent_addr;  //clent_addr用于记录发送方的地址信息
    while(1)
    {
        memset(buf, 0, BUFF_LEN);
        len = sizeof(clent_addr);
        count = recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, &len);  //recvfrom是拥塞函数,没有数据就一直拥塞
        if(count == -1)
        {
            printf("recieve data fail!\n");
            return;
        }
        printf("client:%s\n",buf);  //打印client发过来的信息
        memset(buf, 0, BUFF_LEN);
        sprintf(buf, "I have recieved %d bytes data!\n", count);  //回复client
        printf("server:%s\n",buf);  //打印自己发送的信息给
        sendto(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, len);  //发送信息给client,注意使用了clent_addr结构体指针

    }
}


/*
    server:
            socket-->bind-->recvfrom-->sendto-->close
*/

int main(int argc, char* argv[])
{
    int server_fd, ret;
    struct sockaddr_in ser_addr;

    server_fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET:IPV4;SOCK_DGRAM:UDP
    if(server_fd < 0)
    {
        printf("create socket fail!\n");
        return -1;
    }

    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,需要进行网络序转换,INADDR_ANY:本地地址
    ser_addr.sin_port = htons(SERVER_PORT);  //端口号,需要网络序转换

    ret = bind(server_fd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    if(ret < 0)
    {
        printf("socket bind fail!\n");
        return -1;
    }

    handle_udp_msg(server_fd);   //处理接收到的数据

    close(server_fd);
    return 0;
}

5.3.2 Client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 512
#define SERVER_IP "172.0.5.182"


void udp_msg_sender(int fd, struct sockaddr* dst)
{

    socklen_t len;
    struct sockaddr_in src;
    while(1)
    {
        char buf[BUFF_LEN] = "TEST UDP MSG!\n";
        len = sizeof(*dst);
        printf("client:%s\n",buf);  //打印自己发送的信息
        sendto(fd, buf, BUFF_LEN, 0, dst, len);
        memset(buf, 0, BUFF_LEN);
        recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&src, &len);  //接收来自server的信息
        printf("server:%s\n",buf);
        sleep(1);  //一秒发送一次消息
    }
}

/*
    client:
            socket-->sendto-->revcfrom-->close
*/

int main(int argc, char* argv[])
{
    int client_fd;
    struct sockaddr_in ser_addr;

    client_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(client_fd < 0)
    {
        printf("create socket fail!\n");
        return -1;
    }

    memset(&ser_addr, 0, sizeof(ser_addr));
    ser_addr.sin_family = AF_INET;
    //ser_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //注意网络序转换
    ser_addr.sin_port = htons(SERVER_PORT);  //注意网络序转换

    udp_msg_sender(client_fd, (struct sockaddr*)&ser_addr);

    close(client_fd);

    return 0;
}

5.4 高级udp socket编程

5.4.1 udp的connect函数

  • UDP网络编程中的connect函数,仅仅用于表示确定了另一方的地址,并没有其他含义。

  • UDP套接字有以下区分:
    1)未连接的UDP套接字
    2)已连接的UDP套接字

  • 未连接的套接字,使用的是sendto/recvfrom进行信息的收发,目标主机的IP和端口是在调用sendto/recvfrom时确定;

  • 在一个未连接的UDP套接字上给两个数据报调用sendto函数内核将执行以下六个步骤:
    1)连接套接字
    2)输出第一个数据报
    3)断开套接字连接
    4)连接套接字
    5)输出第二个数据报
    6)断开套接字连接

  • 已连接的UDP套接字,先经过connect来向目标服务器进行指定,然后调用read/write进行信息的收发,目标主机的IP和端口是在connect时确定。

  • 已连接的UDP套接字给两个数据报调用write函数内核将执行以下三个步骤:
    1)连接套接字
    2)输出第一个数据报
    3)输出第二个数据报

  • connect函数的UDP通信框架
    image

connect函数处理:

void udp_handler(int s, struct sockaddr* to)
{
    char buf[1024] = "TEST UDP !";
    int n = 0;
    connect(s, to, sizeof(*to);
 
    n = write(s, buf, 1024);
 
    read(s, buf, n);
}

5.4.2 udp报文丢失问题

UDP服务器客户端中,客户端发送的数据丢失,服务器会一直等待,直到客户端的合法数据过来。如果服务器的响应在中间被路由丢弃,则客户端会一直阻塞,直到服务器数据过来。

防止永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,大概有两种方法:
1)使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。

2)使用select为recvfrom设置超时
设置select函数的第五个参数即可。

5.4.3 udp报文乱序问题

乱序就是发送数据的顺序和接收数据的顺序不一致。例如发送数据的顺序为A、B、C,接收到的数据顺序却为:A、C、B。产生这个问题的原因在于每个数据报走的路由不一样,有的路由顺畅,有的却拥塞,导致每个数据报到达目的地的顺序就不一样。UDP协议并不保证数据报的按序接收。

解决这个问题的方法就是发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报。

5.4.4 udp流量控制问题

UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。

解决方法一般采用增大UDP缓冲区,使得接收方的接收能力大于发送方的发送能力。

int n = 220 * 1024; //220kB
setsocketopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

网络抓包示例

具体流程:

比如服务器开启9502的端口。使用tcpdump来抓包:

tcpdump -iany tcp port 9502

然后使用telnet 127.0.0.1 9502开连接:

telnet 127.0.0.1 9502

14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 0(1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0 (2)
14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0 (3)


14:13:01.415407 IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7
14:13:01.415432 IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0
  • 114:12:45.104687 时间带有精确到微妙
  • localhost.39870 > localhost.9502 表示通信的流向,39870是客户端,9502是服务器端
  • [S] 表示这是一个SYN请求
  • [S.] 表示这是一个SYN+ACK确认包:
  • [.] 表示这是一个ACT确认包, (client)SYN->(server)SYN->(client)ACT 就是3次握手过程
  • [P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推
  • [F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起
  • [R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接
  • win 4099 是指滑动窗口大小
  • length 18指数据包的大小
    可以看到 (1)(2)(3)三步是建立tcp:

第一次握手:

14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378

客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(syn=j)到服务器》

syn包(syn=j) : syn的seq= 2927179378 (j=2927179378)

第二次握手:

14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,

收到请求并确认:服务器收到syn包,并必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包:
此时服务器主机自己的SYN:seq:y= syn seq 1721825043。
ACK为j+1 =(ack=j+1)=ack 2927179379

第三次握手:

14:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1,

客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)

客户端和服务器进入ESTABLISHED状态后,可以进行通信数据交互。此时和accept接口没有关系,即使没有accepte,也进行3次握手完成。

连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。

IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7

客户端向服务器发送长度为7个字节的数据,

IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0

服务器向客户确认已经收到数据

IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18

然后服务器同时向客户端写入数据。

IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

客户端向服务器确认已经收到数据

tcp可靠的连接,每次通信都需要对方来确认

posted @ 2023-07-12 20:22  登云上人间  阅读(37)  评论(0编辑  收藏  举报