目录
理解源IP地址和目的IP地址
认识端口号
端口号
理解源端口号和目的端口号
套接字
认识TCP与UDP协议
网络字节序
socket编程接口
socket 常见API
sockaddr结构
理解源IP地址和目的IP地址
就如同我们唐僧的取经路:
- 唐僧的出发地到目的地:东土大唐 -> 西天。
- 唐僧的长途跋涉途中:东土大唐 -> …… -> 车迟国 -> 黑风岭 -> …… ->西天。
- 源IP -> 目的IP:是永远都不会改变的。
- 源MAC -> 目的MAC:会根据当前所处的位置,不断的进行变化。
目的MAC是受目的IP的影响的,以此到达目地的IP。
认识端口号
我们所使用IPV4,在经过互联网的大爆发,人人拥有上网设备,甚至是几个上网设备,而公司之类的也需要配IP。仅仅IPV4的32位字节,肯定是远远的不够的,而现在之所以还能正常的使用。原因是存在一种技术,叫做NAT技术,其划分了对应的局域网和公网。
而不论是在什么网段,在各自的特定网段当中,IP都要表示该对应主机的唯一性。
#问:把数据送到对方的机器上是目的吗?
不是的,就如同我们在使用一个app的时候,如:抖音,其所播放的视频内容是经过网络申请,然后抖音的服务器将视频内容数据发送给抖音app,那么为什么不是发送给淘宝app偏偏就是要发送给抖音app?就是因为是软件客户端(抖音)申请的数据,然后将数据申请通过给我们的主机,让主机(手机)帮它去申请,所以数据到主机都还是没有结束的。
- 主机(手机):上层的软件客户端在跑。
- 服务器:上层的服务器软件在跑。
所以通讯的时候,并不是两个机器在通讯,而是二者上层的软件在通讯。而对应的软件就是程序员所写的对应的代码,编译所形成的二进制程序。所以准确的来说,因为客户端也是需要打开才哪能申请数据、接收数据。
(对于上面的问题)不是的,真正的网络通信过程,本质上其实是进程间通信!将数据在主机间转发仅仅是手段,(一般在网络通讯的时候真正网络通讯的本质是进程间通讯,但是因为跨主机了,所以我们需要将数据先在主机间进行通讯)机器收到之后,需将数据交付给指定的进程!
#问:将数据教给对应的主机之后,如何保证的主机可以将对应的数据交付给特定的进程?
利用端口号!
端口号
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
端口号是标识特定主机的上的网络进程的唯一性!即:端口号在特定的主机上有若干个进程,端口号不能够被同时绑定,只能够被一个进程进行关联。
所以任何一个发出的报文:一定有IP,port(端口号)。
#问:在Linux的进程中,有一个进程PID,进程PID也标定了进程的唯一性,那进程PID与端口号的关系是什么?
重点是他们标识进程唯一性的能力,进程PID是进程管理的范畴,如果网络也使用进程PID来标识唯一性,那么网络的模块和进程管理的模块就黏在一起了,就会导致两个毫不相关的模块产生关联这是没有任何意义的,单独实现一个端口号提供给网络模块使用,实现网络模块与其他的模块的解耦。
并且不是所有进程都需要端口号,但是是一定所有进程都需要进程PID。
#问:一个进程可以和多个端口号绑定吗?
一个进程可以和多个端口号绑定,我们想绑定几个就绑定几个都没有任何问题。只要一个端口号只能和一个进程关联即可。
理解源端口号和目的端口号
为了让我们进行更好的数据通信,客户端有自己的端口,服务端也有自己的端口。客户端向服务器发送消息,客户端的端口叫做源端口,服务端的端口叫做目的端口。如果是服务端向客户端发数据,客户端的端口叫做目的端口,服务端的端口叫做源端口。
谁发数据谁就是源,谁收数据谁就是目的。
套接字
- SRC_IP:标定互联网中的唯一的一台主机。
- SRC_PORT:标定主机中唯一的进程。
套接字:SRC_IP + SRC_PORT。
网络通讯的本质:进程间通信,套接字编程。
认识TCP与UDP协议
传输层是处于操作系统的,并且是离上层应用层最近的,应用层所使用的接口都是TCP协议 / UDP协议提供的。
认识UDP协议
- 传输层协议
- 无连接 —— 不用写代码的时候刻意连接,直接发送数据
- 不可靠传输 —— 可能会出现网络丢包的问题 / 数据包重复乱序等问题
- 面向数据报
认识TCP协议
- 传输层协议
- 有连接—— 写代码的时候需要刻意连接
- 可靠传输
- 面向字节流
UDP协议就如同发短信,无需经过对方的同意,直接发送即可。TCP协议就如同打电话,需要经过对方的同意,不能直接进行通话,需要对方接听。
#问:为什么UDP协议不可靠,却还是要选择UDP协议?
现在的网络中,出现丢包的问题概率不大,即便出现了丢包问题,有些场景下也是可以容忍的,更重要的是不可靠是特点,可靠也是特点,但是不可靠看起来像缺点,但是它的反面就是没有为了让对应的程序,变得可靠,而做了更多得工作。可靠性是需要付出大量的编码,和数据得处理的,设置更多的策略,更复杂。
TCP协议就是什么都要管,而UDP协议就是只要将数据发出去了就行了。一个更安全,一个更简单。在日常编写中,除非十分的适合UDP协议,其余情况都是使用TCP协议。比如:视频、直播等,在播放的时候出现声音丢失一下、卡一下,就有可能是使用UDP协议,而出现的丢包的情况。
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。
什么是大端小端:
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
#问:那么如何定义网络数据流的地址呢?
首先,我们需要知道,两天主机如果大小端不一致,经过网络通讯时必然会导致双方无法正常的通信。
解决这个问题,猜测做法:
无论是A -> B发,B -> A发:都把自己的大小端信息特征带上,对方主机提取数据时就先对大小端特征 —— 也很逗,因为大小端特征也是对应的存储,怎么识别?
于是网络就直接规定了:
网络规定:所有的网络数据,都必须是大端!
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
- 如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
- h:host,n:network,l:32位长整数,s:16位短整数。(h开头代表主机转网络)
- 例如:htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动。
socket编程接口
socket 常见API
// 创建 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);
常见的套接字:
- 域间socket —— 本主机内的进程间通信,也称作:基于套接字式的管道通信。
- 原始socket —— 通常用于编写很多很多的工具,一般我们在应用层写代码用的是传输层接口,原始socket允许我们绕过传输层,直接使用网络层,甚至使用网络层以下的底层。
- 网络socket
因为套接字的不同,所以理论上,是三种应用场景,对应的应该是三套接口。但是Linux在设计的时候,不想设计过多的接口,于是就将所有的接口进行了统一。
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同。
#:sockaddr结构的出现。
在上述图中,以及前面将常见的三种套接字的介绍,套接字不仅需要支持跨网络的进程间通信,还需要支持本地的进程间通信(域间socket)。在进行跨网络通信时我们需要传递的端口号和IP地址,所以就是必须使用上述的 struct sockaddr_in 结构体。而本地通信(域间socket)则不需要,因此使用上述的 struct sockaddr_un 结构体,其中类型对应的不同,涉及的参数最终导致的是需要两个接口。就是因为 struct sockaddr_in 结构体是用于跨网络通信的,而 struct sockaddr_un 结构体是用于本地通信的,是不同的。
所以,为了让套接字的网络通信和本地通信能够使用同一套函数接口,于是就出现了 struct sockaddr 结构体,该结构体与 struct sockaddr_in 和 struct sockaddr_un 的结构都不相同,但这三个结构体头部的16个比特位都是一样的,叫做地址类型,就是表明其是网络通信的套接字还是本地通信的套接字。
可以说: struct sockaddr 很像一个基类, struct sockaddr_in 和 struct sockaddr_un 是其的派生类。所以我们的传递完全可以传递 struct sockaddr ,最终是什么结构体,完全看完全看结构体头部的16个比特位的对应。
总结:
-
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
-
IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。