文章目录
-
- 一、预备知识
-
- [1.1 重新理解网络通信的本质](#1.1 重新理解网络通信的本质)
- [1.2 认识端口号](#1.2 认识端口号)
-
- [1.2.1 端口号范围划分](#1.2.1 端口号范围划分)
- [1.2.2 端口号VS进程ID](#1.2.2 端口号VS进程ID)
- [1.2.3 理解源端口号和目的端口号](#1.2.3 理解源端口号和目的端口号)
- [1.3 理解socket](#1.3 理解socket)
- [1.4 简单认识TCP协议和UDP协议](#1.4 简单认识TCP协议和UDP协议)
-
- [1.4.1 TCP协议](#1.4.1 TCP协议)
- [1.4.2 UDP协议](#1.4.2 UDP协议)
- [1.5 网络字节序](#1.5 网络字节序)
- 二、socket编程接口
-
- [2.1 socket常见API](#2.1 socket常见API)
- [2.2 sockaddr结构](#2.2 sockaddr结构)
一、预备知识
1.1 重新理解网络通信的本质
思考一个问题:在进行网络通信时,数据传输到主机是目的吗?不是的。因为数据是给人用的,不是给机器用的!
比如:聊天是人在聊天,下载是人在下载,浏览网页是人在浏览?
人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的QQ、迅雷、浏览器。
而启动的QQ、迅雷、浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于拿到了数据。
所以:数据传输到主机不是目的,而是手段。到达主机内部,再交给主机的进程,才是目的。
得出网络通信的本质:就是进程间的通信。
- 网络协议中的下三层,主要解决的是:数据安全可靠的送带远端机器。
- 用户使用应用层的软件(把这个软件(进程)启动起来),完成数据的发送和接收。
但是在系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程呢?这就要在网络中唯一标识某一个主机的某一个进程------端口号。
1.2 认识端口号
端口号(port)是传输层协议的内容。
- 端口号是一个2字节的16位整数。
- 端口号用来唯一标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- IP地址+端口号就能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
1.2.1 端口号范围划分
- 0~1023:为知名端口号,HTTP、FTP、SSH等这些广为使用的应用层协议,它们端口号都是固定的,占用在这个范围的端口号。
- 1024~65535:操作系统动态分配的端口号,客户端程序的端口号,就是由操作系统从这个范围分配的。
我们在日常开发时,切忌不要去占用0~1024这个范围的端口号,这个范围端口号大概率被其他进程占用了。
1.2.2 端口号VS进程ID
我们知道,PID表示唯一一个进程;此处我们的端口号也是表示唯一一个进程,那为什么还要搞一个端口号?
有以下两个原因:
- 不是所有的进程都要网络通信,但是所有进程都要有PID。
- 系统和网络之间功能的解耦。
可以类比学号和身份证号之间的关系来理解。
- 不是所有的人都要有学号,但是所有人都要有身份证号。
另外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定,是1对多的关系 。
进程 : 端口号 = 1 : n 进程:端口号 = 1:n 进程:端口号=1:n
进程 ID 属于系统概念,技术上也具有唯一性,确实可以用来标识唯一的一个进程,但是这样做,会让系统进程管理和网络强耦合,实际设计的时候,并没有选择这样做。
1.2.3 理解源端口号和目的端口号
传输层协议(TCP 和 UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。就是在描述 "数据是谁发的, 要发给谁"。
在网络服务中,通常是客户端向服务器发起服务请求,哪客户端如何知道服务端的端口号是多少??
因为每一个服务器的端口号必须是众所周知的,精心设计的,被客户端所知晓的。
1.3 理解socket
- 综上,IP地址用来标识网络中唯一一台主机,而port用来标识主机上的唯一一个网络进程。
- IP+Port就能表示网络中的唯一一个进程。
- 所以,
{源IP地址、目的IP地址、源端口号、目的端口号}这样的四元组就可以标识网络中唯二的两个进程 - 网络通信的本质,就是进程间通信
- socket套接字本质就是网络之间的进程间通信的方式。
进程间通信的方式除了管道、消息队列、信号量、共享内存等方式外,还有套接字,只不过前者是不跨网络的,而后者是跨网络的。 - 我们把IP+Port叫做socket
1.4 简单认识TCP协议和UDP协议
网络协议栈是贯穿整个体系结构的,在应用层、操作系统层和驱动层各有一部分。当我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信。

而传输层最典型的两种协议就是TCP协议和UDP协议。
1.4.1 TCP协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,未来学习传输层时,再详细讨论TCP的细节问题。
TCP协议是面向连接的、可靠的、面向字节流的传输层的协议。
TCP协议是面向连接的,如果两台主机之间相应进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。另外,TCP协议是保证可靠性的,数据在传输过程若出现了丢包、乱序等情况,TCP都要对应的解决方案。
1.4.2 UDP协议
此处我们也先对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识,未来学习传输层时,再详细讨论UDP的细节问题。
UDP协议是无连接的、不可靠的、面向数据报的传输层的协议。
使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
既然UDP协议是不可靠的,那为什么还要有UDP协议的存在?
TCP协议是一种可靠的传输协议,使用TCP协议能够在一定程度上保证数据传输时的可靠性,而UDP协议是一种不可靠的传输协议,UDP协议的存在有什么意义?
首先,不可靠在技术上是中性词,不是说可靠就一定比不可靠更好,我们不能带有偏见来看待事物。
- 可靠是需要我们做更多的工作的,TCP虽然是一种可靠性传输协议,但这一定意味着TCP协议在底层需要做更多的工作,因此TCP协议底层的实现是比较复杂的,这是TCP的一个缺点。
- UDP协议虽然是一种不可靠的传输协议,但这一定意味着UDP协议在底层不需要做过多的工作,因此UDP协议底层的实现一定比TCP协议要简单,UDP协议虽然不可靠,但是它能够快速的将数据发送给对方,虽然在数据在传输的过程中可能会出错。
- 编写网络通信代码时具体采用TCP协议还是UDP协议,完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,此时我们就必须采用TCP协议,如果应用场景允许数据在传输出现少量丢包,那么我们肯定优先选择UDP协议,因为UDP协议足够简单。
1.5 网络字节序
主机在存储数据时有大小端的概念:
- 小端机器:数据的高字节内容保证在内存的高地址处,数据的低字节内容保存在内存的低地址处。
- 大端机器:数据的高字节内容保证在内存的低地址处,数据的低字节内容保存在内存的高地址处。
如果是本地的数据传输,不需要考虑大小端问题,因为同一台机器数据采用的数据存储方式一定是一样的,要么采用的都是大端存储模式,要么采用的都是小端存储模式。但如果是网络通信,那就必须考虑大小端的问题,因为两台主机数据存储的方式可能不同,就可能会造成对端主机识别出来的数据可能与发送端想要发送的数据是不一致。
例如,现在两台主机之间在进行网络通信,其中发送端是小端机,而接收端是大端机。发送端将发送缓冲区中的数据按内存地址从低到高的顺序发出后,接收端从网络中获取数据依次保存在接收缓冲区时,也是按内存地址从低到高的顺序保存的。

但由于发送端和接收端采用的分别是小端存储和大端存储,此时对于内存地址从低到高为44332211的序列,发送端按小端的方式识别出来是0x11223344,而接收端按大端的方式识别出来是0x44332211,此时接收端识别到的数据与发送端原本想要发送的数据就不一样了,这就是由于大小端的偏差导致数据识别出现了错误。
保证通信双方存储数据的方式一样是不现实的,所以TCP/IP协议规定:网络数据流统一采用大端字节序。
- 如果发送端是小端,需要先将数据转成大端,然后再发送到网络当中。
- 如果发送端是大端,则可以直接进行发送。
- 如果接收端是小端,需要先将接收到数据转成小端后再进行数据识别。
- 如果接收端是大端,则可以直接进行数据识别。
注意:大小端的转化工作是由操作系统来完成的,因为该操作属于通信细节,不过也有部分的信息需要我们自行进行处理,比如端口号和IP地址。
为什么网络字节序采用大端?不用小端?
该问题有很多不同说法,下面列举了两种说法:
- 说法一: TCP在Unix时代就有了,以前Unix机器都是大端机,因此网络字节序也就采用的是大端,但之后人们发现用小端能简化硬件设计,所以现在主流的都是小端机,但协议已经不好改了。
- 说法二: 大端序更符合现代人的读写习惯。
网络字节序与主机字节序之间的转换
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,系统提供了四个函数,可以通过调用以下库函数实现网络字节序和主机字节序之间的转换。
cpp
#include <arpa/inet.h>
// 主机字节序转网络字节序
uint32_t htonl(uint32_t hostlong); // 32位长整数类型
uint16_t htons(uint16_t hostshort); // 16位短整数类型
// 网络字节序转主机字节序
uint32_t ntohl(uint32_t hostlong); // 32位长整数类型
uint16_t ntohs(uint16_t hostshort); // 16位短整数类型
辅助记忆:函数名当中的h表示host主机,n表示network网络,l表示32位长整数,s表示16位短整数。
二、socket编程接口
2.1 socket常见API
- 创建socket文件描述符:
cpp
// TCP/UDP, 客户端+服务器
int socket(int domain, int type, int protocal);
- 绑定端口号:
cpp
// TCP/UDP, 服务器
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);
- 开始监听socket:
cpp
// TCP, 服务器
int listen(int sockfd, int backlog);
- 接收连接请求:
cpp
// TCP, 服务器
int accept(int sockfd, struct sockaddr* address, socklen_t* address_len);
- 建立连接:
cpp
// TCP, 客户端
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
2.2 sockaddr结构
socket套接字不仅支持跨网络的进程间通信(网络套接字),还支持本地进程间通信(域间套接字)。网络通信需要我们传递端口号和IP地址,而本地通信不需要,因此套接字提供了sockaddr_in结构体和sockaddr_un结构体,其中sockaddr_in用于跨网络通信,sockaddr_un用于本地通信。
而为了能让socket套接字的网络通信和本地通信能够共用同一套API,于是就出现了sockaddr结构体,该结构体与sockaddr_in和sockaddr_un都不同,但这三个结构体头部的16个比特位都是一样的,这个字段叫做协议家族。

此时当我们在传递在传参时,就不用传入sockeaddr_in或sockeaddr_un这样的结构体,而统一传入sockeaddr这样的结构体。在设置参数时就可以通过设置协议家族这个字段,来表明我们是要进行网络通信还是本地通信,在这些API内部就可以提取sockeaddr结构头部的16位进行识别,进而得出我们是要进行网络通信还是本地通信,然后执行对应的操作。此时我们就通过通用sockaddr结构,将套接字网络通信和本地通信的参数类型进行了统一。
温馨提示 :实际我们在网络编程时,定义的还是
sockadd_in这样的结构体指针,在接口中传参时将该结构体的类型强转为sockaddr*(因为他们大小是一样的)。