👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
一、端口号
1.1 概念
首先我们需要明确的是,两台主机之间的通信不仅仅是数据的传输,更重要的是实现了对端主机上的具体服务的访问。比方说我们用户在用抖音刷视频时,不仅仅是想将我们的请求发送给对端服务器,更是访问和利用对端服务器上运行的特定服务——视频服务。
也就是说,网络通信本质上就是两个进程之间在进行通信,只不过这里是跨网络的进程间通信。比如手机上的淘宝和抖音应用进程通过网络与对端服务器上的淘宝服务进程和抖音服务进程进行交互,完成从请求到响应的完整通信流程。这个过程涉及了多个层次的协议和技术,但其基本的通信模式仍然是进程间通信的一个扩展。
因此,进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字socket
,只不过前者是不跨网络的(基于单主机的),而后者是跨网络的。
通过网络协议栈来利用网络资源,让两个不同的进程看到同一个资源。
那以上说的这些和端口号有什么关系呢?
在网络通信中,尤其是涉及多个进程的情况下,每台主机上可能有多个应用程序(进程)同时进行网络通信。例如,一台主机上可能同时使用HTTP
协议与Web
服务器通信和FTP
协议进行文件传输。那如何确保数据能够准确地从一个主机上的进程传输到另一个主机上的正确进程,并在通信结束后,确保响应数据能够准确地返回到原始的发起进程。
因此,端口号port
的作用实际就是标识一台主机上的一个网络应用层的进程,其作用就是让数据能够准确地从发送进程传递到目标进程,并确保响应数据能够正确地返回。 那就必须要有源端口号和目标端口号。
- 源端口号:当客户端进程向服务器发送请求时,会在数据包中包含一个源端口号。这是客户端进程使用的端口号。
- 目标端口号:数据包中还包含一个目标端口号,用于指示服务器上哪个进程应该处理这个数据包。
其它说明:
- 端口号是一个
2
字节16
位的整数。 - 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
IP
地址 + 端口号能够标识网络上的某一台主机的某一个进程。- 一个端口号只能被一个进程占用。
网络通信本质就是进程间通信,而进程间通信需要标识哪两个进程在通信,由于IP
地址能够唯一标识公网内的一台主机,而端口号能够唯一标识一台主机上的一个进程。因此,用 IP
地址+端口号就可以标识公网环境下的唯一的网络进程。
当数据在传输层进行封装时,就会添加上对应源端口号和目的端口号的信息。这时通过源IP
地址+源端口号就能够在网络上唯一标识发送数据的进程,通过目的IP
地址+目的端口号就能够在网络上唯一标识接收数据的进程,此时就实现了跨网络的进程间通信。
而这种基于IP
地址+端口号的通信方式,我们称为socket
。socket
在英文上有插座的意思,插座上有不同规格的插孔,我们将插头插入到对应的插孔当中就能够实现电流的传输。因此,那些老外可能就想表达:在进行网络通信时,客户端就相当于插头,服务端就相当于一个插座,但服务端上可能会有多个不同的服务进程(多个插孔),因此当我们在访问服务时需要指明服务进程的端口号(对应规格的插孔),才能享受对应服务进程的服务。
一个进程可以绑定多个 端口号 吗?一个 端口号 可以被多个进程绑定吗?
端口号是隶属于某台主机的,所以端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复,不然就无法区别要把数据发给哪个进程。
而如果一个进程绑定多个 端口号,依然可以保证唯一性,因为无论使用哪个端口号,数据始终只会交给一个进程;但如果一个端口号被多个进程绑定了,在信息递达时,是无法分辨该信息的最终目的进程的,存在二义性。端口号的作用是配合IP
地址标识网络世界中进程的唯一性!!!
底层如何通过端口号找到对应进程的?
当接收方的传输层接收到数据包后,它需要确定数据包应该交给哪个应用程序(进程)处理。而端口号本身只是在网络协议中使用的标识,不能直接与应用层进程通信。因此,操作系统会维护一个端口号和进程pid
的映射表(类似于哈希表),当底层拿到端口号时就可以直接执行对应的哈希算法,然后就能够找到该端口号对应的进程,最后就可以进行通信了。
1.2 端口号 vs 进程pid
端口号
port
的作用唯一标识一台主机上的某个进程,进程pid
的作用也是唯一标识一台主机上的某个进程,那在进行网络通信时为什么不直接用pid
来代替port
呢?
首先需要明确的是:
- 进程
pid
是用来标识系统内所有进程的唯一性的,它是属于系统级的概念。 - 而端口号
port
是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。
一台机器上可能会有大量的进程,每个进程都必须要有自己的pid
。但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程(如守护进程),好像统一使用pid
区分不开。
并且如果在网络中使用pid
,会导致网络标准中被迫中引入进程管理相关概念,那么就会导致进程管理与网络强耦合,而网络本身就很复杂,再将pid
与网络标准结合会增加系统的复杂度,可能影响网络协议的可维护性。而端口号的引入就是为了实现系统和网络解耦。
就比如每个人都有自己的身份证号,身份证号已经可以标识我们的唯一性了,但是当我们到了学校还是会有学号,到了公司还是会有工号。这是为什么呢?为什么不直接用身份证号来代替学号和工号呢?
因为身份证号是国家用于管理人民时用的编号,而学号是学校用于管理学生时用的编号,工号是公司用于管理员工时用的编号。但并不是全中国人都在某所学校或某家公司,因此在学校或公司当中,没必要用身份证号来标识每个人的唯一性。此时就出现了学号和工号,在学号和工号当中还可以包含一些便于管理的信息,比如入学(入职)年份、性别等信息。
也就是说,不同的场景,使用不同的编号来标识某种事物的唯一性可能更加合适 ~
二、传输层协议
主流的传输层协议有两个:TCP
和 UDP
。两个协议各有优缺点,可以采用不同的协议,实现截然不同的网络程序,关于TCP
和UDP
的详细信息将会放到后面的博客中详谈,先对这两种协议有一个直观的认识:
-
TCP
协议:传输控制协议(Transmission Control Protocol
)- 面向连接(建立连接)
- 可靠传输
- 面向字节流(字节流就像水龙头,用户可以根据自己的需求获取水流量)
-
UDP
协议:用户数据报协议(User Datagram Protocol
)- 无连接
- 不可靠传输
- 面向数据报(数据报相当于包裹,用户每次获取的都是一个或多个完整的包裹)
关于可靠性
-
TCP
的可靠传输并不意味着它可以将数据百分百递达,而是说它在数据传输过程中,如果发生了传输失败的情况,它会通过自己独特的机制来保证可靠(做更多的事),比如重新发送数据等方式。因此TCP
协议底层的实现是比较复杂的。我们不能只看到TCP
协议面向连接可靠这一个特点,也要能看到TCP
协议对应的缺点。 -
UDP
虽然是一种不可靠的传输协议,但这一定意味着UDP
协议在底层不需要做过多的工作,只管发就完事了,出现丢包、乱序啥的也不关我的事。所以传输速度很快。因此,UDP
协议底层的实现一定比TCP
协议要简单。
TCP
和UDP
的使用场景
编写网络通信代码时,虽然TCP
协议具有可靠性,但也不能无脑选择TCP
,如果真是这样的话,UDP
就不会出现在教科书上了。因此,具体采用TCP
协议还是UDP
协议,完全取决于上层的应用场景。
- 如果应用场景严格要求数据在传输过程中的可靠性,即需要保证数据完整性和顺序的场景。此时我们就必须采用
TCP
协议。比如金融交易、网页请求、文件传输等。 - 如果应用场景允许数据在传输出现少量丢包,或者对传输速度要求较高的领域(对实时性要求高)。那么我们肯定优先选择
UDP
协议。比如短视频、直播等。
注意: 一些优秀的网站在设计网络通信算法时,会同时采用TCP
协议和UDP
协议,当网络流畅时就使用UDP
协议进行数据传输,而当网速不好时就使用TCP
协议进行数据传输,此时就可以动态的调整后台数据通信的算法。
三、网络字节序
回顾大小端
数据拥有高权值位和低权值位,比如在32
位操作系统中,十六进制数0x11223344
,其中的11
称为最高权值位,44
称为 最低权值位
计算机在存储数据时是有大小端的概念的:
- 大端字节序(大端模式): 将数据的高权值存放在内存的低地址处,低权值存放在高地址处。
- 小端字节序(小端模式): 将数据的高权值存放在内存的高地址处,低权值存放在低地址处。
如果编写的程序只在本地机器上运行,那么是不需要考虑大小端问题的,因为同一台机器上的数据采用的存储方式都是一样的。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。
由于我们不能保证通信双方存储数据的方式是一样的。因此,网络当中传输的数据必须考虑大小端问题。必须要有一个标准。因此,TCP/IP
协议规定:网络中传输的数据,统一采用大端存储方案,即低地址高字节,也就称为网络字节序。而现在大端/小端称为主机字节序。无论是大端机还是小端机,都必须按照TCP/IP
协议规定的网络字节序来发送和接收数据。
- 发送方:如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。
- 接收方:接收方需要将接收到的数据从网络字节序转换为主机字节序,以便应用程序可以正确地解释和处理这些数据。
系统提供了四个函数,可以通过调用以下库函数实现网络字节序和主机字节序之间的转换。
#include <arpa/inet.h>
// 将32位整数转换为网络字节序
uint32_t htonl(uint32_t hostlong);
// 将16位整数转换为网络字节序
uint16_t htons(uint16_t hostshort);
// 将32位网络字节序整数转换为主机字节序
uint32_t ntohl(uint32_t netlong);
// 将16位网络字节序整数转换为主机字节序
uint16_t ntohs(uint16_t netshort);
// l 表示32位长整数
// s 表示16位短整数
四、socket套接字
2.1 常见API
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
2.2 sockaddr结构
套接字编程的种类:
- 域间套接字(本地的进程间通信)
- 原始套接字
- 网络套接字编程
套接字不仅支持跨网络的进程间通信,还支持本地的进程间通信;原始套接字主要是用来编写网络工具。域间套接字和原始套接字我们不详谈。我们重点谈网络套接字编程,它主要是利用传输层协议(如TCP
和UDP
)来实现进程间通信。
在进行跨网络通信时,需要我们传递端口号和IP
地址,而本地通信则不需要。因此套接字提供了 sockaddr_in
结构体 和 sockaddr_un
结构体:
sockaddr_in
结构体是用于跨网络通信sockaddr_un
结构体是用于本地通信
为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了sockeaddr
结构体,该结构体与sockaddr_in
和sockaddr_un
的结构都不相同,但这三个结构体头部的16
个比特位都是一样的,这个字段叫做协议家族。
- 可以根据
16
位地址类型,判断是网络通信,还是本地通信 - 在进行网络通信时,需要提供
IP
地址、端口号等网络通信必备项。而本地通信只需要提供一个路径名,通过文件读写的方式进行通信(类似于管道)
注意: 实际我们在进行网络通信时,还是要定义sockaddr_in
和sockaddr_un
这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*
。这是使用C
语言实现多态的典型做法,确保该标准的通用性。
为什么参数没有用
void*
代替struct sockaddr*
类型?
因为在该网络接口设计时,C
语言还不支持void*
这种类型,只能将套接字接口设置为struct sockaddr*
作为通用指针类型。那现在支持了为什么不改呢?
尽管现代C
语言支持void*
,但为了保持向前兼容性,并避免对现有代码造成破坏。如果真的现在将接口参数类型修改为void*
,那以前写的那些代码全部都要修改!
关于socketaddr_in
结构的更多详细信息放到后面写代码时再细谈