目录
一、源IP和目的IP
二、端口号
三、UDP协议和TCP协议
四、网络字节序
五、socket编程
1、socket 常见接口
2、struct sockaddr结构体
一、源IP和目的IP
IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
每台主机都有自己的IP地址,所以当数据从一台主机传输到另一台主机就需要IP地址,数据传输时,报头里面就包含了IP地址。为什么呢?因为在现在的互联网世界,每台主机的IP地址都是唯一的,而且是全球唯一的,所以一个IP地址能够标识世界上一个唯一的一个主机。于是,我们就能通过IP地址找到目的主机了。
源IP地址:发送数据报那个主机的IP地址,目的IP地址:想发送到的那个主机的IP地址。
二、端口号
但是,我们的最终目的仅仅是想把信息从一台主机发送到另一台主机吗?当然不是,我们最终的目的应该是将信息发送到主机上的软件的!
举个例子,你点开了抖音客户端,就相当于在手机上启动了一个进程,那么字节跳动公司的服务器将会把抖音的内容,通过网络传输给你的手机,但是不会仅仅传到手机上就好了,而是会将信息传给手机上的抖音客户端,也就是你启动的一个进程!
那么为什么抖音的服务器知道把信息传给抖音客户端,而不是美团或者qq客户端呢?这就是我们端口号的作用了。
我们使用端口号来标识某个主机上的一个进程!于是,抖音服务器通过IP地址找到主机,通过唯一的端口号找到主机上的抖音客户端进程,就能够把信息推送给抖音客户端,让用户就能够看到了。
端口号:
1、端口号是一个2字节16位的整数。
2、端口号用来标识一个进程,告诉操作系统要把数据交给哪一个进程。
3、在同一个主机中,一个端口号只能被一个进程占用。
所以说,通过全网唯一IP地址能够找到唯一的主机,然后通过端口号能够标识网络上的某一台主机的某一个进程,那么我们通过(IP地址,端口号)的组合,就能够找到全网唯一的进程了!那么网络通信的本质就是进程间的通信!而我们把(IP地址,端口号)的组合称为套接字。
注:一个端口号只能被一个进程占用,但是一个进程可以绑定多个端口号。
三、UDP协议和TCP协议
TCP和UDP协议是TCP/IP协议的核心。他们都属于传输层协议。
~ TCP协议特点:
有连接(正式通信前要先建立连接)
可靠传输
面向字节流
~ UDP协议特点:
无连接
不可靠传输
面向数据报
如果发送数据时出现了丢包的情况、或者数据被重复传递了(传递了多份)、或者网络出现了问题等等造成的后果就叫做不可靠。
与此相对,可靠传输则通过各种机制(如确认信息、重传等)来确保数据的完整性和可靠性。TCP就是一种可靠的传输,但是它的可靠一定是通过大量的代码去维护实现的。当然,UDP虽然是不可靠传输,但是它的使用成本低。我们应该根据不同的场景,去选择协议。
四、网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分。小端:低权值的数放入低地址。大端:低权值的数放入高地址。
正是因为有了大小端的区分,我们就有了这样一个问题:两台主机在进行网络通信的时候,如果一个主机是大端机,而另一个主机是小端机,那么因为两个主机对于数据在内存中的存储方式不一样,必定会解析错误对方发过来的信息。
所以就有了这样的规定:在进行网络传输的时候,网络中的数据都是大端。发送数据的主机如果是大端机就不用管,如果是小端机就把小端转成大端再发送。接收数据的主机收到的也是大端的,然后将数据转换成自己能够识别的字节序。
为了方便,我们可以调用库函数进行网络字节序和主机字节序之间的转换:
#include <arpa/inet.h>
// 主机序列转网络序列
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
// 网络序列转主机序列
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
比如,h表示host,n表示network,l表示32位长整数,s表示16位短整数。 htons就表示由主机字节序转成网络字节序,且为短整型。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
五、socket编程
1、socket 常见接口
下面的接口会在后面详细讲解使用。
// 创建 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、struct sockaddr结构体
上面的函数中,有一个参数 struct sockaddr 结构体很重要,我们需要重点知道。
IP地址和端口号port,我们称为套接字。而套接字种类比较多。我们常见的有以下三种:
1、网络套接字 2、原始套接字 3、unix域间套接字
原始套接字可以跨过传输层(TCP/IP协议)访问底层的数据。域间套接字就是我们常用来进行同主机的进程间通信的,也是属于进程间通信的一种,我们在之前在讲进程间通信的时候,也提到了套接字的通信方式。
今天我们主要了解进行网络通信的网络套接字。(网络套接字也能进行本地通信)
这些套接字应用场景完全不同,所以我们想用就得用三套不同的接口,但是他们的结构和操作方式又比较相似,所以为了方便,设计者只设计了一套接口,就可以通过传入不同的参数,解决所有网络或者其他场景下的通信问题。
比如,网络套接字和域间套接字。
套接字的通用结构就是上图的第一个:struct sockaddr,也就是socket相关函数里面的参数的结构。
struct sockaddr_in就是网络套接字的结构,struct sockaddr_un就是域间套接字的结构。
我们使用struct sockaddr的前2个字节来区分该套接字是网络套接字还是域间套接字,AF_INET代表网络套接字,AF_UNIX代表域间套接字。
struct sockaddr_in里面,包含了端口号和IP地址。struct sockaddr_un里面,就是包含了路径名,通过相同路径下的同一个文件来让进程看到同一份资源,相当于进程间通信的管道一样。
对于网络通信,函数的参数是const struct sockaddr *addr,但实际传递进去的却是sockaddr_in结构体,所以我们在使用时一定要进行类型强转。
注:我们可以把struct sockaddr看成基类,把sockaddr_in和sockaddr_un看成派生类,这样构成了多态。相当于使用相同的 struct sockaddr 结构达到了不同的作用。