Socket – 套接字 计算机之间进行通信的一种约定或一种方式。
典型应用:Web服务器和浏览器:浏览器获取用户输入的URL,向服务器发起请求,服务器分析接收到URL,将对应的网页内容返回给浏览器,浏览器经过分析和渲染,将文字图片,视频等元素呈现给用户。
(背景了解啦。)
IP地址
计算机不知道IP地址对应的地理位置,通信时,将IP地址封装道要发送的数据中,交给路由器去处理。
目前用IPv4较多,IPv6在教育网中用的比较多。
端口(PORT)
IP 可以找到目标计算机,但是依旧不能通信,一台计算机提供多种网络服务,web FTP服务,SMTP服务(简单邮件传输协议)。计算机需要知道把数据包给哪个网络程序,端口的作用来了。
端口(port)一道窗口,数据通过它进出,每个窗口都有编号,即端口号。
web: 80
FTP: 21
SMTP 25
数据传输方式:
- SOCK_STREAM 表示面向连接的数据传输方式,数据可以准确无误的到达另一台计算机,如果数据损坏或者丢失,可以重新发送,相对来说效率较慢。常见的http协议就是使用SOCK_STREAM来传输数据,因为它 可靠!!!(有没有想到TCP)
- SOCK_DGRAM 表示无连接的数据传输方式,它没有上述有点,发出去它就不管了,所以来说效率是高的,但是不可靠。(数据错误概率也是不高的)(联想到UDP)
( 例如视频聊天,快是主要的,丢失点数据,比如卡顿一下,无伤大雅,这时候就是取最优问题)
注意: 有可能多种协议都使用同一种数据传输方式,所以在socket编程中,需要同时指明数据传输方式和协议。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
//创建套接字
//AF_INET 表示使用IPv4地址。
//SOCK_STREAM 表示使用面向连接的数据传输方式
// IPPROTO_TCP 使用TCP协议。
//socket也是一种文件,有文件描述符,可以使用write()/read()函数进行I/O操作
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//将套接字和IP、端口绑定
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//bind()函数将套接字sev_sock,与特定的ip地址和端口绑定,ip地址和端口保存在socaddr_in结构体中。 socket()函数确定了套接字的各种属性,bind()函数让套接字与特定的IP地址和端口对应起来,这样才能让客户端连接到该套接字。
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//进入监听状态,等待用户发起请求
//被动监听状态,套接字一直处于睡眠中,一直到客户端发起请求才会被唤醒
listen(serv_sock, 20);
//接收客户端请求
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
//accept()函数用来接受客户端的请求,程序一旦执行道accept()就会被堵塞(暂停运行),知道客户端发起请求。
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
//向客户端发送数据
char str[] = "Hello World!";
//write()函数用来向套接字文件中写入数据,也就是向客户端发送数据。
write(clnt_sock, str, sizeof(str));
//关闭套接字
//socket使用完毕后也要用close()关闭。
close(clnt_sock);
close(serv_sock);
return 0;
}
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(){
//创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
//向服务器(特定的IP和端口)发起请求
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//区别于server,通过connect()向服务器发起请求,服务器的ip地址和端口号保存在sockaddr_in
结构体中,直到服务器传回数据后,connect()才运行结束。
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//读取服务器传回的数据
char buffer[40];
//read()从套接字文件中读取数据。
read(sock, buffer, sizeof(buffer)-1);
printf("Message form server: %s\n", buffer);
//关闭套接字
close(sock);
return 0;
}
编译运行server,程序运行到accept函数会堵塞,等待客户端发起请求,接下来编译client,此时会通过connect函数向client传回数据,client接收到传回的数据,connect()便会结束,使用read (),读出数据。然后程序全部结束。
省略掉window下的socket程序
细节方面的差别
- windows 下的socket程序依赖winsock.dll和ws2_32.dll,必须提前加载。DLL加载方式
- linux 使用 文件描述符而windows 使用 文件句柄
- linux 不区分socket 文件和普通文件,windows 区分。
- linux 下socket()函数的返回值为int,windows下为SOCKET类型,也就是句柄。
- linux 下使用 read()/write()读写,而Windows 使用recv()/send()函数发送和接受
- 关闭socket ,linux用close() 。而Windows 使用closesocket()函数。
WSAStartup()函数
windows socket编程依赖于系统提供的动态链接库(DLL)
较早的DLL市wsock32.dll,大小为28k,对应头文件为winsock1.h;
最新的DLL是ws2_32.dll,大小为69k,对应的头文件winsock2.h;
#pragma comment (lib,“ws2_32.lib") 编译时加载。
WSASartup()函数
使用DLL之前,需要调用WSASartup()函数进行初始化,指明WinSock规范的版本
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested 为 WinSock 规范的版本号,低字节为主版本号,高字节为副版本号(修正版本号);lpWSAData 为指向 WSAData 结构体的指针。
wVersionRequested 参数用来指明我们希望使用的版本号,它的类型为 WORD,等价于 unsigned short,是一个整数,所以需要用 MAKEWORD() 宏函数对版本号进行转换。例如
MAKEWORD(1, 2); //主版本号为1,副版本号为2,返回 0x0201
MAKEWORD(2, 2); //主版本号为2,副版本号为2,返回 0x0202
关于WSAData结构体
typedef struct WSAData {
WORD wVersion; //ws2_32.dll 建议我们使用的版本号
WORD wHighVersion; //ws2_32.dll 支持的最高版本号
//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的实现以及厂商信息
char szDescription[WSADESCRIPTION_LEN+1];
//一个以 null 结尾的字符串,用来说明 ws2_32.dll 的状态以及配置信息
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets; //2.0以后不再使用
unsigned short iMaxUdpDg; //2.0以后不再使用
char FAR *lpVendorInfo; //2.0以后不再使用
} WSADATA, *LPWSADATA;
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")
int main(){
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData);
printf("wVersion: %d.%d\n", LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));
printf("wHighVersion: %d.%d\n", LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
printf("szDescription: %s\n", wsaData.szDescription);
printf("szSystemStatus: %s\n", wsaData.szSystemStatus);
return 0;
}
运行结果:
运行结果:
wVersion: 2.2
wHighVersion: 2.2
szDescription: WinSock 2.0
szSystemStatus: Running
WinSock编程的第一步就是加载ws2_32.dll,然后调用WSAStartup()函数进行初始化,并指出要使用的版本号。
Linux 创建socket
linux中,啥都是文件,除了文本文件,源文件,二进制文件,一个硬件设备也可以映射为一个虚拟的文件,成为设备文件,例如stdin成为标准输入文件,它对应的硬件设备一般是键盘,stdout成为标准输入文件,它对应的硬件设备一般是显示器,对于所有的文件,都可以使用read()函数读取数据,使用write()数据写入函数。
一切都是文件的思想简化了程序员的理解和操作,使得对硬件设备的处理就像普通文件一样,所有在linux中创建的文件都有一个int类型的编号,成为文件描述符(File Descriptor)。使用文件时,只要知道文件描述符就可。 例如stdin 的描述符为0,stdout的描述符为1 。
Linux中,socket也被认为时文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用I/O相关的函数,可以认为,两台计算机之间的通信,实际上就是两个Socket 文件的相互读写
Linux下使用<sys/socket.h>头文件中socket()函数来创建套接字
int socket(int af,int type,int protocol)
- af为地址族(address family)也就是ip地址类型,常用 的有 AF_INET 和AF_INET6,
address family 对应AF ; internet 对应INET ; AF_INET对应ipv4;AF_INET6对应ipv6;
127.0.0.1 是一个特殊的ip地址,表示本机地址。
2. type为数据传输方式,常用的有SOCK_STREAM和SOCK_DGRAM。
(面向连接 面向无连接)
3. protocol就是协议嘛,常用的有IPPROTO_TCP 和IPPROTO_UDP传输协议。
这地方不知道大家有没有疑问,按理说,前两个参数感觉已经够了对不对
除非两种不同的协议支持同一种地址类型和数据传输类型,我们不指明那种使用协议,操作系统无法推演。
附: 若只有一种协议可以满足条件,那protocol的值可以设置为零,
例如
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
Windows 创建socket
Windows 下也使用 socket() 函数来创建套接字,原型为:
SOCKET socket(int af, int type, int protocol);
除了返回值类型不同,其他都是相同的。Windows 不把套接字作为普通文件对待,而是返回 SOCKET 类型的句柄。请看下面的例子:
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
//创建TCP套接字
Bind()函数
bind()函数的原型为
int bind(int sock,struct sockaddr *addr,socklen_t addrlen);//linux
int bind(SOCKET sock, const struct sockaddr *addr,int addrlen);//Windows
sock为 socket 文件描述符, addr 为sockaddr 结构体变量的指针,addrlen为addr 变量的大小,可以通过 sizeof()计算出来。
int sever_sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//创建sockaddr_in 结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr))l//每个字节都用0填充。
serv_addr.sin_family =AF_INET;//使用ipv4地址
serv_addr.sin_addr.s_addr=inet_addr("127.0.0.1");//具体的ip地址
serv_addr.sin_port=htons(1234);//端口
//将套接字和IP、端口绑定。
//我们使用的是sockaddr_in结构体,然后在这个地方强制转换为sockaddr 类型。为什么捏
bind(ser_sock,(struct sockaddr*)&serv_addr, sizeof(serv_addr);
sockaddr_in 结构体
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
- sin_family 和 socket()的第一个参数的含义相同,取值也要保持一致。
- sin_port 为端口号。 unit16_t的长度为两个字节,理论上端口号的取值范围为065536,但是01023的端口一般由系统分配给特定的服务程序,例如web 服务的端口号为80,FTP服务的端口号为21,所以程序分配端口号一般在1024开始。
- sin_addr是struct in_addr结构体类型的变量。
- sin_zero[8]是多余的八个字节,没有啥用,一般使用memset()函数填充为0.
上述代码就是用memset()函数将结构体的全部字节填充为0,再给前3个成员复制,剩下的sin_zero 自然就是0;
in_addr 结构体
sockaddr_in的第三个成员是in_addr类型的结构体,该结构体只有一个成员
struct in_addr{
in_addr_t s_addr; 32位的ip地址
}
in_addr_t 在头文件<netinet/in.h>中定义,等价于unsigned long 长度为4个字节,也就是说 s_addr 是一个整数, 而ip地址是一个字符串,所以需要inet_addr() 函数进行转换,所以
unsigned long ip = inet_addr("127.0.0.1");
printf("%ld\n", ip);
运行结果 16777343
为什么要这样做呢,结构体中嵌套结构体,而不是用sockaddr_in 中的一个成员变量来指明IP地址,socket()函数的第一个函数已经指明了地址类型,为什么在sockaddr_in 结构体再说一次呢???
介个是历史遗留问题。。。。。。
为什么使用sockaddr_in 而不适用sockaddr
bind()第二个参数类型为sockaddr ,而代码中使用sockaddr_in ,然后强制转换为sockaddr ,为什么捏??
sockaddr 结构体:
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
下图是两种结构体对比,括号内代表占用字节数量。
二者长度都是16字节,但是将ip地址和端口号合并道一起,用sa_data表示,要想赋值要同时指明ip地址和端口号,例如“127.0.0.1:80” ,但是没有相关函数将这个字符串转换成需要的形式,也就很难给sockaddr类型的变量赋值,所以使用 sockaddr_in 来代替这两个结构体长度相同,强制转换类型时候不会丢失字节,也没有多余的字节。
总结:sockaddr是一种通用的结构体,可以用来保存多种类型的ip地址和端口号,而 sockaddr_in是专门用来保存ipv地址的结构体,还有sockaddr_in6,用来保存ipv6地址,定义如下:
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6
in_port_t sin6_port; //(2)16位端口号
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具体的IPv6地址
uint32_t sin6_scope_id; //(4)接口范围ID
};
也恰恰因为通用结构体sockaddr使用不变,所以才针对不同的地址类型定义了不同的结构体。
connect()函数
connect()函数用来建立连接,原型
int connect(int sock,struct sockaddr*serv_addr,socklen_t addrlen);//Linux
int connect(SOCKET sock,const struct sockaddr*serv_addr,int addrlen);//Windows
listen()函数
通过listen 函数可以让套接字进入被动监听状态,原型是
int listen(int sock,int backlog);//linux
int listen(SOCKET sock,int backlog);//Windows
sock 为需要进入监听状态的套接字,backlog为请求队列的最大长度。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被唤醒来响应请求。
请求队列
当套接字正在处理客户端请求时,如果有新的请求进来,套接字时没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理,如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满,这个缓冲区就被成为请求队列(Request Queue)。
缓冲区的长度可以通过listen()函数和backlog参数指定,并发量小的话可以是10或20.
如果将backlog的值设置为SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。
当请求队列满的时候,就不再接受新的请求,对于linux,客户端会收到ECONNREFUSED错误,对于Windows,客户端会收到WSAECONNREFUSED错误。
listen()只是让套接字处于监听状态,并没有接受请求,接受请求需要使用accept()函数。
accept()函数
当套接字处于监听状态时,可以通过accept()函数来接受客户端请求,原型
int accept(int sock,struct sockaddr *addr,socklen_t *addrlen);//linux
SOCKET accept(SOCKET sock,struct sockaddr*addr,int *addrlen);//Windows
accept()返回一个新的套接字来和客户端通信,addr保存了客户端的IP地址和端口号,而sock是服务器端的套接字,注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
listen()只是让套接字进入监听状态,并没有真正接受客户端请求,listen()后面的代码继续执行,知道遇到accept(),accept()会阻塞程序执行(后面代码不能被执行),知道有新的请求进来。
LINUX下数据的接收和发送
Linux 不区分套接字文件和普通文件,使用write()可以向套接字中写入数据,使用read()可以从套接字中读取数据。
ssize_t write(int fd, const void *buf, size_t nbytes);
fd为要写入的文件的描述符,buf为要写入的数据的缓冲区地址,nbytes为要写入的数据的字节数。
ssize_t read(int if,void *buf,size_t nbytes);
read()函数会从fd文件中读取nbytes个字节并保存到缓冲区buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败返回-1/
Socket 缓冲区
每个socket被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。
write()/send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管他们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情
(发出去就完事了)
‘
- I/O缓冲区再每个TCP套接字中单独存在;
- I/O缓冲区在创建套接字时自动生成;
- 即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
- 关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是8k,可以通过getsockopt()函数获取:
unsigned optVal;
int optLen = sizeof(int);
getsockopt(servSock, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen);
printf("Buffer length: %d\n", optVal)
运行结果:
Buffer length: 8192
堵塞模式
对于TCP套接字(默认情况下),当使用write()/send()发送数据时:
- 首先会检查缓冲区,如果缓冲区的可用空间长度要小于要发送的数据,那么write()/send()会被堵塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒write()/send()函数继续写入数据。
- 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send()也会被堵塞,知道数据发送完毕缓冲区解锁,write()才会被唤醒
- 如果要写入的数据大于缓冲区的最长长度,会分批写入。
- 所有数据均写入缓冲区的时候 write()/send()才能返回。’
当使用read()/recv()读取数据时:
5. 缓冲区有数据就读取,否则就会堵塞,直到网络上有数据到来
6. 如果要读取的数据小于缓冲区的数据长度,那么就不饿能一次性全部读出,剩余数据不断积压,知道read函数再次读取
7. 知道读取到数据后read才会返回,否则会一直被堵塞。
TCP套接字默认情况下时阻塞模式,也是最常用的,也可以更改为非阻塞。
l
TCP粘包问题以及数据的无边界性
数据的接收和发送是无关的,read()函数不管发送了多少次,都会尽可能多的接收数据,也就是说,read()和write()的执行次数可能不同。
例如 ,write() 重复执行三次,每次都发送字符“abc”,那么目标机器上的read()可能分三次接收,也可能分两次接收,第一次接收“abcab”,第二次接收“cabc”,也可能一次就接收到字符串“abcabcabc”。
假设我们希望客户端每次发送一次学生的学号,让服务器返回该学生的姓名、住址、成绩等信息,这时候可能就会出现问题,服务器端不能区分学生的学号,例如i第一次发送1,第二次发送3,服务器可能当成13来处理,返回的信息显然就是错误的。
这就是数据的粘包问题,客户端发送的多个数据包被当作一个数据包接收,也称数据的无边界性,read()函数不知道数据包的开始或结束标志,只把他们当作连续的数据流来处理。
TCP数据报结构以及三次握手(图解)
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。
客户端在收发数据前要使用connect()函数和服务器建立连接,建立连接的目的是保证IP地址、端口、物理链路等正确无误为数据的传输开辟通道
TCP建立连接时要传输三个数据包,俗称三次握手
TCP数据包结构
序号: Seq序号占32位,用来识别从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。
确认号 Ack确认号占32位,客户端和服务器端都可以发送Ack=Seq+1.
标志位 每个标志位占 1 Bit ,共有6个,URG\ACK\RST\SYN\FIN\PSH
- PSH: 接收方应该尽快将报文交给应用层,优先级很高滴
- URG: 紧急指针有效
- RST:重置连接
客户端调用socket()函数创建套接字后,因为没有建立连接,所以套接字处于closed状态;
服务器端调用listen()函数后,套接字进入listen状态,开始监听客户端请求。
1.当客户端调用connect()函数后,TCP协议会组建一个数据包,并设置SYN标志位,表示该数据包用来建立同步连接的,同时生成一个随机数字1000,填充序号字段,表示该数据包的序列号,完成这些工作,开始向服务端发送数据包,客户端就进入了syn-send状态。
2.服务端收到数据包,检测到已经设置了syn标志位,就知道这是客户端发来的建立连接的请求包,服务器也会组建一个数据包,并设置syn和ack标志位,syn表示该数据包用来建立连接,ACK用来确认收到了刚才客户端来的的数据包。
服务器生成一个随机数2000,填充序号字段,2000和客户端数据包没有关系
服务器将客户端数据包序1000+1 得到1001,并用这个数字填充ack
服务器将数据包发出,进入SYN-RECV状态。
3.客户端收到数据包,检测到已经设置了syn和ack标志位,就知道这是服务器发来的确认包,客户端会检测ack字段,看看它有没有加一,有就是说明连接莫得猫病。
然后捏,客户端在发一个数据包,ack为2000+1,用它来填充确认号字段。
客户端发出,进入ESTABLISED状态,表示连接成功建立。
4.服务器端收到数据包,检测已经设置了ACK标志位,就知道客户端发来了确认,服务器就会检测ack字段是否是2001,说明连接建立成功,服务器进入ESTABLISED状态。
说明
三次握手的关键就是确认对方收到了自己的数据包,这个目标就是通过确认号ack字段实现的,计算机会记录下自己发送的数据包序号Seq,待收到对方的数据包后,检测确认号ack字段,瞅瞅是不是之前自己发出那个序列号加一的值。如果是,就说明对方收到了自己发送的数据包。
TCP连接
此时Ack号为1301而不是1201,原因在于Ack号的增量为传输的数据字节数,假设每次Ack号不加传输的字节数,这样虽然可以确认数据包的传输,但无法明确100字节全部正确传递还是丢失了一部分。ack号=seq号+传递的字节数+1;
最后加一是为了告诉对方要传递的seq号。
中间发生了错误,主机B没有收到,经过一段时间后,主机A仍未收到对于Seq1301的ACK确认,因此尝试重传数据。
为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,超时重传。
重传次数,有些系统,一个数据包只会重传3次,如果重传3次后还未收到该数据包的ACK确认,就不再尝试重传。
最后需要说明的是,发送端只有在收到对方的 ACK 确认包后,才会清空输出缓冲区中的数据
TCP四次握手断开连接(图解)
如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。
建立连接需要三次握手,断开连接需要四次握手,
- 客户端调用close()函数后,向服务器发送FIN数据包,进入FIN_WAIT_1状态。FIN是finish的缩写,表示完成任务需要断开连接。
- 服务器收到数据包检测到了fin,然后发送确认,进入close_wait状态。服务器收到请求不会立即断开,而是向客户端发送确认包。
- 客户端收到确认包进入FIN_WAIT2状态,等待服务器准备完毕后再次发送数据包。
- 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送FIN包,断开连接进入last_ack状态。
- 客户端收到服务器的FIN包后,再向服务器发送ACK包,进入timewait状态。
- 服务器收到客户端的ack,断开连接,关闭套接字。进入closed。
TCP是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B确认,回传ACK包,告诉机器A我收到了。如果机器B没有回传ACK机器A会重新发送。
客户端最后一次回传ack,可能服务器收不到,服务器会再次发送fin包,如果这时候客户端已经关闭了连接,服务器无论如何也收不到ack包了,所以客户端得等待,
等多久???
数据包在网络中有生存时间,超过这个时间还没到主机就会被丢弃,即报文最大生存周期MSL,
ack到服务器需要msl,服务器重传需要msl,2msl是数据包往返的最大时间,如果2msl后还未收到服务器重传的fin包,就说明服务器收到了ack。
大端小端
CPU向内存保存数据的方式有两种:
- 大端序 :高位字节放到低位地址(高位字节在前)
- 小端序:高位字节放在高位地址(低位地址在前)
不同的CPU保存和解析数据的方式不同,大端序和小端序系统通信时会发生数据解析错误,因此在发送数据前,要将数据转换为统一的格式 网络字节序,网络字节序为大端序。
在socket使用域名
客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。而使用域名会方便很多,更换ip地址时修改域名解析即可。
UDP套接字
UDP是非链接的传输协议,没有建立连接和断开连接的过程它只是简单地把数据丢到网络中,不需要ACK包确认。
UDP传输效率很高。
TCP的速度无法超越UDP,但在收发某些类型的数据有可能接近UDP ,每次交换的数据量越大,tcp越接近udp。