新人Socket学习笔记

Socket – 套接字 计算机之间进行通信的一种约定或一种方式。

典型应用:Web服务器和浏览器:浏览器获取用户输入的URL,向服务器发起请求,服务器分析接收到URL,将对应的网页内容返回给浏览器,浏览器经过分析和渲染,将文字图片,视频等元素呈现给用户。
(背景了解啦。)

IP地址

计算机不知道IP地址对应的地理位置,通信时,将IP地址封装道要发送的数据中,交给路由器去处理。
目前用IPv4较多,IPv6在教育网中用的比较多。

端口(PORT)

IP 可以找到目标计算机,但是依旧不能通信,一台计算机提供多种网络服务,web FTP服务,SMTP服务(简单邮件传输协议)。计算机需要知道把数据包给哪个网络程序,端口的作用来了。
端口(port)一道窗口,数据通过它进出,每个窗口都有编号,即端口号。
web: 80
FTP: 21
SMTP 25
在这里插入图片描述
数据传输方式:

  1. SOCK_STREAM 表示面向连接的数据传输方式,数据可以准确无误的到达另一台计算机,如果数据损坏或者丢失,可以重新发送,相对来说效率较慢。常见的http协议就是使用SOCK_STREAM来传输数据,因为它 可靠!!!(有没有想到TCP)
  2. 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程序
细节方面的差别

  1. windows 下的socket程序依赖winsock.dll和ws2_32.dll,必须提前加载。DLL加载方式
  2. linux 使用 文件描述符而windows 使用 文件句柄
  3. linux 不区分socket 文件和普通文件,windows 区分。
  4. linux 下socket()函数的返回值为int,windows下为SOCKET类型,也就是句柄。
  5. linux 下使用 read()/write()读写,而Windows 使用recv()/send()函数发送和接受
  6. 关闭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)
  1. 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填充
};
  1. sin_family 和 socket()的第一个参数的含义相同,取值也要保持一致。
  2. sin_port 为端口号。 unit16_t的长度为两个字节,理论上端口号的取值范围为065536,但是01023的端口一般由系统分配给特定的服务程序,例如web 服务的端口号为80,FTP服务的端口号为21,所以程序分配端口号一般在1024开始。
  3. sin_addr是struct in_addr结构体类型的变量。
  4. 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()发送数据时:

  1. 首先会检查缓冲区,如果缓冲区的可用空间长度要小于要发送的数据,那么write()/send()会被堵塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒write()/send()函数继续写入数据。
  2. 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send()也会被堵塞,知道数据发送完毕缓冲区解锁,write()才会被唤醒
  3. 如果要写入的数据大于缓冲区的最长长度,会分批写入。
  4. 所有数据均写入缓冲区的时候 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

  1. PSH: 接收方应该尽快将报文交给应用层,优先级很高滴
  2. URG: 紧急指针有效
  3. 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四次握手断开连接(图解)

如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。
建立连接需要三次握手,断开连接需要四次握手,
在这里插入图片描述

  1. 客户端调用close()函数后,向服务器发送FIN数据包,进入FIN_WAIT_1状态。FIN是finish的缩写,表示完成任务需要断开连接。
  2. 服务器收到数据包检测到了fin,然后发送确认,进入close_wait状态。服务器收到请求不会立即断开,而是向客户端发送确认包。
  3. 客户端收到确认包进入FIN_WAIT2状态,等待服务器准备完毕后再次发送数据包。
  4. 等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送FIN包,断开连接进入last_ack状态。
  5. 客户端收到服务器的FIN包后,再向服务器发送ACK包,进入timewait状态。
  6. 服务器收到客户端的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。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值