网络通信之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无需建立类如三次握手的连接,通信效率高
三、Socket是什么?
- Socket是
应用层与TCP/IP协议族通信的中间软件抽象层
,是一组接口。
- 套接字描述符是一个整数,与句柄相似。常用句柄中,0是标准输入,1是标准输出,2是标准错误输出。对应的FILE * 结构的表示就是stdin、stdout、stderr
- 当应用程序要为因特网通信创建一个套接字(socket)时,操作系统返回一个小整数作为描述符(descriptor)来标识这个套接字。应用程序以该描述符作为传递参数,通过调用函数来完成某种操作。
- 应用程序使用描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。
系统为每个运行的进程维护一张单独的文件描述符表。
当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表
,并把该表的索引值返回给调用者 。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。
针对套接字的系统数据结构:
- 创建一个套接字:socket函数。单个系统调用可以创建任何套接字。一旦套接字创建后,应用程序需要调用其他函数来指定具体细节。
- 套接字的内部数据结构包含很多字段,应用程序创建套接字后,在该套接字可以使用之前,必须调用其他的过程来填充这些字段。
文件描述符和文件指针的区别:
-
文件描述符
:在linux系统中打开文件获得文件描述符,是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。 -
文件指针
:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄
基本的SOCKET接口函数
- 服务器端初始化Socket,与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。如果客户端初始化Socket,连接服务器(connect),连接成功,客户端与服务器端的连接建立。客户端发送数据请求,服务器端接收请求并处理请求,并把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
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再次确认
4.2 TCP连接的终止(四次握手释放)
- TCP连接是全双工的,每个方向都必须单独进行关闭。
- 原则是当一方完成它的
数据发送
任务后就能发送一个FIN
来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据
。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
接口函数:
-
客户端
A发送一个FIN
,来关闭客户A到服务器B
的数据传送(报文段4)。 -
服务器
B收到这个FIN
,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。(服务器B不能收了,但仍可以发) -
服务器
B关闭与客户端A的连接
,发送一个FIN
给客户端A(报文段6)。 -
客户端
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编程框架
- 客户端要发起一次请求,仅需两个步骤(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通信框架
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可靠的连接,每次通信都需要对方来确认
本文来自博客园,作者:登云上人间,转载请注明原文链接:https://www.cnblogs.com/lj15941314/articles/socketTcpUdp.html