Socket的详细介绍

  • Post author:
  • Post category:其他




前言

本文是关于Socket通信的大杂烩。我深知自己的局限性和不足之处,无论是在语言表达、逻辑思维还是知识储备上,都有许多需要改进和学习的地方。因此,如果您发现我在博客中有任何错误、不准确或者需要补充的地方,欢迎指正并提出建议。



1-Socket出现的原因



1.1-Socket出现的背景

Socket(套接字)是一种用于实现网络通信的编程接口(API),它提供了一种标准化的方式,使得不同操作系统和编程语言之间的应用程序能够相互通信。Socket最初是在BSD(Berkeley Software Distribution)操作系统中开发出来的,目的是为了实现在不同主机之间进行进程间通信。BSD是由加州大学伯克利分校开发的一个Unix操作系统的分支,它对Socket的定义和实现成为了事实上的标准。后来,由于互联网的发展,Socket被广泛应用于网络编程中。

在早期的计算机网络中,通信使用的是不同的协议,这些协议之间缺乏标准化的接口,使得应用程序的编写和移植变得非常困难。为了解决这个问题,一些计算机科学家开始研究如何定义一种标准的通信接口,以便不同的计算机之间能够进行通信。Socket就是在这个背景下诞生的,它提供了一种可移植、可扩展、易于使用的接口,使得应用程序能够在不同的操作系统和计算机之间进行通信。

随着互联网的发展,Socket成为了网络编程中不可或缺的一部分。它被广泛应用于各种网络应用程序中,如Web服务器、电子邮件客户端、聊天程序等。同时,随着计算机硬件和网络技术的不断发展,Socket也不断更新和完善,以适应新的应用场景和需求。



1.2-Socket解决的问题

Socket通信主要是为了解决计算机网络中的进程间通信问题。在网络编程中,有两个进程需要进行通信才能完成特定的任务,这两个进程可能运行在不同的计算机上,也可能运行在同一台计算机上的不同进程中。Socket提供了一种标准化的接口,使得这些进程能够在网络中进行数据交换和通信。具体来说,Socket通信可以解决以下几个方面的问题:

  1. 进程间通信:在同一台计算机上,不同的进程之间需要进行通信,Socket提供了一种标准化的接口,使得进程之间可以通过网络进行通信。
  2. 跨平台通信:不同的计算机、操作系统和编程语言之间需要进行通信,Socket提供了一种可移植的接口,使得应用程序可以在不同的平台上运行并进行通信。
  3. 网络通信安全性:网络通信中存在着信息泄露、数据篡改、拒绝服务攻击等安全问题,Socket可以通过加密、身份认证、防火墙等方式提高通信的安全性。
  4. 通信协议:Socket提供了一种灵活的通信协议,可以根据需要选择不同的协议来满足特定的通信需求,如TCP、UDP等协议。

总之,Socket通信可以为应用程序提供一种标准化、可靠、安全的网络通信方式,使得不同计算机之间的应用程序可以进行数据交换、信息共享和远程控制等操作。



2-Socket的组成及关键点



2.1-What:什么是Socket?


Socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。Socket本质上是一个抽象层

,它是一组用于网络通信的API,包括了一系列的函数和数据结构,它提供了一种标准的网络编程接口,使得应用程序可以在网络中进行数据传输。Socket本身并不是一个具体的实现,而是一个抽象的概念。不同的操作系统和编程语言可以通过不同的方式来实现Socket API。

通过 Socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。socket()函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

Socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。在Unix/Linux系统下,socket也是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:

  • 用 read() 读取从远程计算机传来的数据;

  • 用 write() 向远程计算机写入数据。

    只要用 socket() 创建了连接,剩下的就是文件操作了。

Socket可以类比成电话线路或电线路,就像电话线路或电线路提供了一条可靠的通信通道,使得两个地点之间可以进行语音或数据通信一样,Socket也提供了一条可靠的通信通道,使得两个计算机之间可以进行数据交换和通信。就像我们在打电话或发送信息时需要先建立连接、传输数据,然后再断开连接一样,Socket也需要先建立连接、传输数据,最后再关闭连接。而且就像电话线路或电线路可以支持不同的通信协议和数据类型一样,Socket也可以支持不同的网络协议和数据格式。

在网络编程中,Socket通常被用于实现以下几种类型的网络通信:

  1. 流式Socket(TCP套接字):基于TCP协议,提供面向连接的可靠通信方式,适用于传输大量数据或要求数据完整性和可靠性的应用程序。
  2. 数据报式Socket(UDP套接字):基于UDP协议,提供无连接的不可靠通信方式,适用于数据量小、响应快、实时性要求高的应用程序。
  3. 原始Socket(Raw Socket):提供对网络协议的底层访问,适用于实现特定的网络协议和功能。



2.2-How: Socket通信实现的步骤

Socket通信实现的步骤一般如下:

  1. 创建Socket:使用socket()函数创建一个Socket,指定通信协议、地址族和Socket类型等参数。创建Socket成功后,系统会为该Socket分配一个唯一的Socket描述符(Socket file descriptor)。
  2. 绑定Socket:使用bind()函数将Socket与本地地址和端口绑定,以便客户端可以连接到该Socket。在绑定Socket时,需要指定通信协议、地址族和本地地址等参数。
  3. 监听Socket:使用listen()函数将Socket设置为监听状态,以等待客户端的连接请求。在监听Socket时,需要指定最大连接数等参数。
  4. 接受连接:使用accept()函数接受客户端的连接请求,并创建一个新的Socket用于与客户端进行通信。在接受连接时,需要指定监听Socket和客户端地址等参数。
  5. 发送和接收数据:使用send()函数向对端发送数据,使用recv()函数接收对端发送的数据。在发送和接收数据时,需要指定通信Socket、发送/接收缓冲区和数据长度等参数。
  6. 关闭Socket:使用close()函数关闭Socket,释放系统资源。 以上是基于TCP协议的Socket通信实现步骤。

如果是基于UDP协议的Socket通信,则无需建立连接,只需要在发送和接收数据时指定对端地址和端口即可。



2.3-How: Socket编写流程


针对TCP的Socket编程:


在这里插入图片描述


Socket编写流程

  • 服务端和客户端初始化

    socket

    ,得到文件描述符;
  • 服务端调用

    bind

    ,将 socket 绑定在指定的 IP 地址和端口;
  • 服务端调用

    listen

    ,进行监听;
  • 服务端调用

    accept

    ,等待客户端连接;

  • 客户端

    调用

    connect

    ,向服务端的地址和端口发起连接请求;
  • 服务端

    accept

    返回用于传输的

    socket

    的文件描述符;

  • 客户端

    调用

    write

    写入数据;服务端调用

    read

    读取数据;

  • 客户端

    断开连接时,会调用

    close

    ,那么服务端

    read

    读取数据的时候,就会读取到了

    EOF

    ,待处理完数据后,服务端调用

    close

    ,表示连接关闭。

这里需要注意的是,服务端调用

accept

时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作

监听 socket

,一个叫作

已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

以下是Linux下的简单的socket编程示例:


Linux下的socket演示程序



3-Socket的关键实现


本文论述及实现均在Linux环境中



3.1-socket()函数创建套接字

在 Linux 下使用 <sys/socket.h> 头文件中 socket() 函数来创建套接字,原型为:

int socket(int af, int type, int protocol);

(1)af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。


(注意:127.0.0.1 是特殊的IP地址,表示本机地址)

(2)type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)

(3)protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。

本教程使用 IPv4 地址,参数 af 的值为 PF_INET。如果使用 SOCK_STREAM 传输数据,那么满足这两个条件的协议只有 TCP,因此可以这样来调用 socket() 函数:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

这种套接字称为 TCP 套接字。

如果使用 SOCK_DGRAM 传输方式,那么满足这两个条件的协议只有 UDP,因此可以这样来调用 socket() 函数:

int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  //IPPROTO_UDP表示UDP协议

这种套接字称为 UDP 套接字。

上面两种情况都只有一种协议满足条件,可以将 protocol 的值设为 0,系统会自动推演出应该使用什么协议,如下所示:

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字



3.2-bind() connect() 函数


以下实现均在Linux环境中

socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来,只有这样,流经该 IP 地址和端口的数据才能交给套接字处理。类似地,客户端也要用 connect() 函数建立连接。

bind() 函数的原型为:

int bind(int sock, struct sockaddr *addr, socklen_t addrlen);  //Linux

sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。

sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。另外还有 sockaddr_in6,用来保存 IPv6 地址。


正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

sockaddr 结构体的定义如下:

struct sockaddr{
    sa_family_t  sin_family;   //地址族(Address Family),也就是地址类型
    char         sa_data[14];  //IP地址和端口号
};

sockaddr 的长度是16字节(sin_family占2个字节,sa_data占14个字节),它是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。

这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。


IPv4专用地址保存结构体——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_prot 为端口号。uint16_t 的长度为两个字节,理论上端口号的取值范围为 0~65536,但 0~1023 的端口一般由系统分配给特定的服务程序,例如 Web 服务的端口号为 80,FTP 服务的端口号为 21,所以我们的程序要尽量在 1024~65536 之间分配端口号。端口号需要用 htons() 函数转换。

  3. sin_addr 是 struct 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

  4. sin_zero[8] 是多余的8个字节,没有用,一般使用 memset() 函数填充为 0。上面的代码中,先用 memset() 将结构体的全部字节填充为 0,再给前3个成员赋值,剩下的 sin_zero 自然就是 0 了。


IPv6专用地址保存结构体——sockaddr_in6 结构体,它的成员变量如下:

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
};

connect() 函数用来建立连接,它的原型为:

int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); 

各个参数的说明和 bind() 相同,sock 为 socket 文件描述符,addr 为 sockaddr 结构体变量的指针,addrlen 为 addr 变量的大小,可由 sizeof() 计算得出。



3.3-listen() accept() 函数

​ 对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。


listen() 函数

listen函数的原型为:

int listen(int sock, int backlog);  //Linux

sock 为需要进入监听状态的套接字,backlog 为请求队列的最大长度。

所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。


请求队列

当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。

缓冲区的长度(能存放多少个客户端请求)可以通过 listen() 函数的 backlog 参数指定。

在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。


但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。

如果将 backlog 的值设置为 SOMAXCONN,就由系统来决定请求队列长度,这个值一般比较大,可能是几百,或者更多。

当请求队列满时,就不再接收新的请求,对于 Linux,客户端会收到 ECONNREFUSED 错误。


accept() 函数

当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求。其原型为:

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);  //Linux

它的参数与 listen() 和 connect() 是相同的:sock 为服务器端套接字,addr 为 sockaddr_in 结构体变量,addrlen 为参数 addr 的长度,可由 sizeof() 求得。


注意:

accept() 返回一个新的套接字来和客户端通信,addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,请注意区分。后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。


需要说明的是:

listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。



3.4-write()/read() 函数


Linux下的数据的接收和传送

Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。

两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。

write() 的原型为:

ssize_t write(int fd, const void *buf, size_t nbytes);

fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。

size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 “size_t” 前面加了一个”s”,代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。

write()函数功能:write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。

read() 的原型为:

ssize_t read(int fd, void *buf, size_t nbytes);

fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。

read() 函数功能:read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。



4-Socket的底层原理

Socket 通信的关键底层基础技术主要包括以下几点:


OSI 模型

(Open Systems Interconnection Model):这是一个用于描述网络通信系统的概念模型。它将网络通信划分为七个层次,从物理层到应用层。Socket 通信主要涉及到传输层(TCP/UDP)和应用层。

在这里插入图片描述


TCP/IP 协议族:

Socket 通信主要依赖于 TCP/IP 协议族。TCP/IP 协议族包括一组互相关联的网络协议,例如 IP(Internet Protocol,网络层协议)、TCP(Transmission Control Protocol,传输层协议)、UDP(User Datagram Protocol,传输层协议)等。Socket 通信使用 TCP 或 UDP 协议进行数据传输。


TCP连接建立时的三次握手过程:


TCP 三次握手过程是怎样的?


TCP连接断开时的四次挥手过程:


TCP 四次挥手过程是怎样的?


套接字(Socket):

套接字是一种用于网络通信的编程接口。它允许应用程序通过网络层(如 IP)和传输层(如 TCP 或 UDP)协议进行通信。套接字提供了一种在不同设备之间传输数据的通用方法,使得网络编程更加简单易懂。

流格式套接字(Stream Sockets)也叫“面向连接的套接字”,在代码中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

SOCK_STREAM 有以下几个特征:

  • 数据在传输过程中不会消失;
  • 数据是按照顺序传输的;
  • 数据的发送和接收不是同步的(有的教程也称“不存在数据边界”)。


“数据传输过程不会消失”“数据是按照顺序传输”

可以将 SOCK_STREAM 比喻成一条传送带,只要传送带本身没有问题(不会断网),就能保证数据不丢失;同时,较晚传送的数据不会先到达,较早传送的数据不会晚到达,这就保证了数据是按照顺序传递的。

之所以流格式套接字可以达到高质量的数据传输,因为它使用了TCP协议,TCP协议会控制你的数据按照顺序达到且没有错误。


“数据的发送和接收不同步”

该如何理解?

假设传送带是传送柚子,接收者需要凑齐20个柚子才能装袋,但因为柚子摆放位置不同,传送带可能会把这些柚子分批传送。第一批5个,第二批10个,第三批5个。接收者不需要和传送带保持同步,也不管传送带传送了几批,也不用每到一批就装袋一次,只需要凑够20个柚子再装袋即可。

流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

也就是说,不管数据分几次传送过来,接收端只需要根据自己的要求读取,不用非得在数据到达时立即读取。传送端有自己的节奏,接收端也有自己的节奏,它们是不一致的。

流格式套接字实际的应用场景有什么?浏览器所使用的 http 协议就基于面向连接的套接字,因为必须要确保数据准确无误,否则加载的 HTML 将无法解析。

数据报格式套接字(Datagram Sockets)也叫“无连接的套接字”,在代码中使用 SOCK_DGRAM 表示。

计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为数据报套接字所做的校验工作少,所以在传输效率方面比流格式套接字要高。

可以将 SOCK_DGRAM 比喻成高速移动的摩托车快递,它有以下特征:

  • 强调快速传输而非传输顺序;
  • 传输的数据可能丢失也可能损毁;
  • 限制每次传输的数据大小;
  • 数据的发送和接收是同步的(有的教程也称“存在数据边界”)。

众所周知,速度是快递行业的生命。用摩托车发往同一地点的两件包裹无需保证顺序,只要以最快的速度交给客户就行,而且由于天气恶劣或人为失误,快递就有可能会延迟或遗失包裹。这种方式存在损坏或丢失的风险,而且包裹大小有一定限制。因此,想要传递大量包裹,就得分配发送。

另外,用两辆摩托车分别发送两件包裹,那么接收者也需要分两次接收,所以“数据的发送和接收是同步的”;换句话说,接收次数应该和发送次数相同。

总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。

数据报套接字也使用 IP 协议作路由,但是它不使用 TCP 协议,而是使用 UDP 协议(User Datagram Protocol,用户数据报协议)。

QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 来传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。


IP、MAC和端口号——网络通信中确认身份信息的三要素


IP 地址:

IP地址是 Internet Protocol Address 的缩写,译为“网际协议地址”。IP 地址是用于标识网络设备的数字地址。在 IPv4(Internet Protocol version 4)中,IP 地址由 4 个字节组成,例如 192.168.1.1。在 IPv6(Internet Protocol version 6)中,IP 地址由 8 组 16 位十六进制数组成,例如 2001:0db8:85a3:0000:0000:8a2e:0370:7334。在因特网上进行通信时,必须要知道对方的 IP 地址。实际上数据包中已经附带了 IP 地址,把数据包发送给路由器以后,路由器会根据 IP 地址找到对方的位置,完成一次数据的传递。


MAC地址

:MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。

现实的情况是,一个局域网往往才能拥有一个独立的 IP;换句话说,IP 地址只能定位到一个局域网,无法定位到具体的一台计算机。这可怎么办呀?这样也没法通信啊。

其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。

数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。


端口(Port):

端口号是一个用于标识网络服务或应用程序的数字。

有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number)。端口号范围从 0 到 65535,其中 0 到 1023 通常保留给系统或者众所周知的服务(如 HTTP、FTP 等)。例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:

在这里插入图片描述



5-Socket的优劣

Socket通信的优势:

  1. 网络通信协议的灵活性:Socket可以支持各种网络通信协议,如TCP、UDP、HTTP等,具有很强的通用性和灵活性,可以满足不同的网络应用需求。

  2. 数据传输的可靠性:Socket提供了面向连接的通信方式,可以保证数据传输的可靠性和完整性,避免数据丢失、重复和损坏等问题。

  3. 系统资源的高效利用:Socket可以使用多线程和多进程技术,实现并发处理多个客户端的请求,充分利用系统资源,提高通信效率和吞吐量。

  4. 程序设计的灵活性:Socket可以使用各种编程语言和工具进行开发,程序设计灵活性强,可以根据具体需求进行定制化开发。

Socket通信的劣势:

​ 1.网络通信的安全性问题:Socket通信存在网络安全方面的问题,如数据窃听、篡改、伪造等,需要采取一些安全措施来保障通信安全。

​ 2.网络环境的不稳定性:Socket通信受到网络环境的影响,如网络延迟、丢包、拥塞等,会影响通信效率和可靠性。

​ 3.系统资源占用较高:Socket通信需要占用一定的系统资源,如内存、CPU等,如果同时处理大量的客户端请求,可能会占用大量的系统资源,导致系统负载过高。



6-Socket调优



6.1-Socket通信的错误处理

进行Socket通信时,可能会出现各种错误,例如网络异常、连接中断、超时等,为了保证程序的稳定性和可靠性,需要进行Socket通信的错误处理,具体方法如下:

  1. 检测错误:在进行Socket通信时,需要检测每个Socket操作的返回值,如果返回值小于0,则表示发生了错误。

  2. 处理错误:对于发生的错误,需要进行相应的处理,例如打印错误信息、关闭Socket连接、重试Socket操作等。

  3. 恢复连接:在Socket连接中断时,需要进行连接恢复的操作,例如重新连接、重试连接等。

  4. 超时处理:对于Socket操作超时的情况,需要进行相应的处理,例如重新进行Socket操作、关闭Socket连接等。

  5. 使用异常处理机制:在进行Socket通信时,可以使用异常处理机制来处理异常情况,例如使用try-catch语句捕获异常并进行相应的处理。

  6. 记录日志:在进行Socket通信时,可以记录日志来跟踪错误和调试程序,以便快速定位和解决问题。



6.2-Socket通信常见的优化方法

Socket通信常见的优化方法包括以下几个方面:

  1. 使用非阻塞式Socket:非阻塞式Socket可以避免等待Socket返回数据时程序出现阻塞的情况,从而提高程序的并发性能和响应速度。

  2. 使用多线程或多进程:通过使用多线程或多进程的方式,可以将Socket通信分配到不同的线程或进程中处理,从而提高程序的并发性能和响应速度。

  3. 使用异步I/O:异步I/O可以在等待Socket返回数据的同时,执行其他操作,可以提高程序的并发性能和响应速度。

  4. 调整TCP参数:通过调整TCP参数可以改善Socket通信的性能,例如调整TCP窗口大小、超时时间等,还有提升TCP三次握手、四次挥手、传输数据的性能。(参考:

    如何优化TCP?

  5. 使用缓存技术:使用缓存技术可以减少Socket通信中的数据传输次数,从而提高程序的吞吐量和响应速度。

  6. 使用零拷贝技术:零拷贝技术可以在数据传输过程中减少数据的复制次数,从而减少CPU和内存的消耗,提高程序的性能和吞吐量。

  7. 减少Socket连接的建立和关闭次数:Socket连接的建立和关闭过程会消耗一定的资源和时间,减少Socket连接的建立和关闭次数可以提高程序的性能和稳定性。

  8. 优化数据传输格式:优化数据传输格式可以减少数据传输量,从而提高程序的吞吐量和响应速度。

Socket通信的优化需要根据具体的应用场景和需求进行调整,需要综合考虑网络带宽、数据传输量、网络延迟、系统资源等因素,以达到最优的性能和稳定性。



7-Socket适用的场景

Socket 通信是一种基于网络的底层通信方式,允许不同设备之间实现数据传输。它适用于多种业务场景和技术场景。以下分别列举了一些典型的应用:


业务场景


  1. 即时通讯

    :聊天应用、在线客服等需要实时高效地传递文本、语音、图片等多媒体信息的场景。

  2. 实时监控

    :物联网设备、工业传感器等需要实时收集和传输监控数据的场景。

  3. 远程控制

    :远程桌面控制、无人机遥控等需要实时传输指令和状态信息的场景。

  4. 文件传输

    :支持大文件传输的应用,如 FTP 或者其他基于 Socket 的文件传输协议。


技术场景


  1. TCP 通信

    :传输控制协议 (TCP) 是一种面向连接、可靠的、基于字节流的通信协议。TCP Socket 通信广泛应用于需要可靠数据传输的场景,例如 HTTP、SMTP 等协议。

  2. UDP 通信

    :用户数据报协议 (UDP) 是一种无连接、不可靠的、基于数据报的通信协议。UDP Socket 通信适用于实时性要求较高,允许丢失部分数据的场景,例如实时音视频通信、在线游戏等。

  3. Unix 域 Socket 通信

    :Unix 域 Socket 用于同一主机上的进程间通信 (IPC),相较于 TCP/UDP,它提供了更高效的通信方式。

  4. Web Socket 通信

    :Web Socket 协议是一种在单个 TCP 连接上进行全双工通信的协议。它适用于需要实时双向数据传输的 Web 应用场景,如在线聊天、实时数据推送等。

  5. 加密通信

    :对于需要安全传输数据的场景,可以使用 SSL/TLS 对 Socket 通信进行加密,例如 HTTPS、IMAPS 等协议。

  6. 多路复用与负载均衡

    :使用 Socket 通信实现多路复用 (Multiplexing) 和负载均衡,提高网络资源利用率和服务可用性。



8-参考资料

《TCP/IP详解 卷1:协议》

小林coding

C语言中文网—Socket通信



9-Socket高频问题

1.什么是Socket通信,它的作用是什么?

2.Socket通信的两种协议是什么?它们有什么区别?

3.Socket通信的五种基本操作是什么?

4、Socket通信中的阻塞和非阻塞模式有什么区别?

5.Socket通信中的TCP和UDP协议有什么区别?它们应该在什么场景下使用?

6.什么是服务器和客户端,它们在Socket通信中的作用是什么?

7.如何进行Socket通信的错误处理?

8.Socket通信中的数据传输方式有哪些?它们的优缺点是什么?

10.如何进行Socket通信的性能优化?有哪些常见的优化方法?



版权声明:本文为XieHYBlog原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。