网络通信之socket工作原理

作者:@登云上人间

目录

一、进程通信
二、什么是TCP/IP、UDP?
三、Socket是什么?
针对套接字的系统数据结构:
基本的SOCKET接口函数
socket()函数
bind()函数
listen()、connect()函数
accept()函数
read()、write()等函数
close()函数
四、Socket TCP
4.1 Socket中TCP的建立(三次握手)
4.2 TCP连接的终止(四次握手释放)
4.3 Socket TCP实例
4.3.1 Server.c
4.3.2 client.C
五、Socket UDP
5.1 UDP编程框架
5.2 UDP程序设计常用函数
5.2.1 socket函数
5.2.2 sendto函数
5.2.3 recvfrom函数
5.2.4 bind函数
5.2.5 close函数
5.3 UDP通用框架搭建
5.3.1 Server.c
5.3.2 Client.c
5.4 高级udp socket编程
5.4.1 udp的connect函数
5.4.2 udp报文丢失问题
5.4.3 udp报文乱序问题
5.4.4 udp流量控制问题
网络抓包示例

一、进程通信

  • 网间进程通信 解决不同主机进程间的相互通信问题
  • 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 0114: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 0214:12:45.104711 IP localhost.39870 > localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0314: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 @   登云上人间  阅读(49)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
阅读排行:
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 易语言 —— 开山篇
· Trae初体验
历史上的今天:
2021-07-12 STM32中断学习笔记
  1. 1 原来你也在这里 周笔畅
  2. 2 世间美好与你环环相扣 柏松
  3. 3 起风了 吴青峰
  4. 4 极恶都市 夏日入侵企划
  5. 5 パレード ヨルシカ
原来你也在这里 - 周笔畅
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

作词 : 姚谦

作曲 : 中島みゆき

编曲 : Terence Teo

制作人 : 朱敬然

请允许我尘埃落定

请允许我尘埃落定

用沉默埋葬了过去

满身风雨我从海上来

才隐居在这沙漠里

该隐瞒的事总清晰

千言万语只能无语

爱是天时地利的迷信

喔 原来你也在这里

啊 那一个人

是不是只存在梦境里

为什么我用尽全身力气

却换来半生回忆

若不是你渴望眼睛

若不是我救赎心情

在千山万水人海相遇

喔 原来你也在这里

请允许我尘埃落定

请允许我尘埃落定

用沉默埋葬了过去

满身风雨我从海上来

才隐居在这沙漠里

该隐瞒的事总清晰

千言万语只能无语

爱是天时地利的迷信

喔 原来你也在这里

啊 那一个人

是不是只存在梦境里

为什么我用尽全身力气

却换来半生回忆

若不是你渴望眼睛

若不是我救赎心情

在千山万水人海相遇

喔 原来你也在这里

啊 那一个人

啊 那一个人

是不是只存在梦境里

为什么我用尽全身力气

却换来半生回忆

若不是你渴望眼睛

若不是我救赎心情

在千山万水人海相遇

喔 原来你也在这里

该隐瞒的事总清晰

千言万语只能无语

爱是天时地利的迷信

喔 原来你也在这里

OT: AISARERU HANA AISAREXIU HANA

OT: AISARERU HANA AISAREXIU HANA

(中文版:原来你也在这里)

OP: Yamaha Music Publishing Inc

SP:百代音乐版权代理(北京)有限公司

配唱制作人:翁乙仁

点击右上角即可分享
微信分享提示