前言:本篇内容将要正式进入网络的编程当中。 本篇的目的是为了能够看完就可以上手写一些网络代码了。 但是本篇也并不会单纯的只讲接口, 前面还是会铺垫一些理论知识更好的认识网络传输。下面, 开始我们的学习吧!
ps:本篇内容的某些接口不好理解,建议先认识一下ip以及网络协议分层这些网络基础知识再来学习。
目录
网络协议栈
我们进行网络通信的时候,我们可能会认为是两台机器在通信。 但是其实,当我们进行通信的时候, 就比如我们下载了一个抖音, 一个微信,一个qq。这些软件我们不去启动它, 我们的机器不耗费流量, 更不会通信。 当我们启动这些软件, 我们的机器才能够进行通信, 并且消耗流量, 这说明什么呢? ——这说明我们真正通信的是应用层, 是应用层想要通信, 然后推动者下层的机器(其实就是下层的操作系统)进行数据一层一层向下交付, 然后由以太网甚至经过路由器中转分发给目标主机。
所以, 我们现在理解的是:
- 网络协议中的下三层(传输, 网络, 数据链路), 主要解决数据安全可靠的送到远端机器。
- 用户使用应用层软件, 完成数据发送和接受的。 (用户使用应用层软件, 先要把这个软件启动起来。 软件启动起来的本质就是进程。)
所以我们日常通信其实是进程间在通信。
即, 日常通信的本质其实就是——进程间通信,只不过是网络层面的进程间通信。——而进程间通信的本质就是让两个进程看到同一份资源, 这里面的同一份资源是谁呢? 在这里其实就是我们的网络。同样的, 我们以前的进程间通信使用个中创建管道, 读写管道,关闭管道的接口,而这里我们的网络同样要给上层提供系统调用来使用网络。而使用的网络资源我们认为系统提供的叫做网络协议栈。
所以, 网络通信的本质其实就是通过网络协议栈使用网络资源, 让两个不同的进程看到了同一份资源。 一个从网络里面写, 一个从网络里面读。
端口号
假如我们的手机上面有一个微信, 我们知道这个微信其实是一个客户端。 然后在厂家那里必定还有一个微信的服务端。 那么当我们用自己的手机发送数据的时候, 数据通过一些列交付过程与网络传送到了微信的服务端。 然后又从微信的服务端传回来。 这个过程中, 为什么我们手机上的微信发送数据, 数据能够精准的到达对面的微信服务端呢? 又为什么微信服务端发回数据, 数据能准确地到微信客户端呢? 要知道, 我们的应用层的应用软件有很多, 为什么数据偏偏到了微信, 而不是到qq呢? 我们的传输层怎么知道要把数据交给哪个应用呢? 所以, 这里的微信就必须和传输层协商一种规定, 这个规定, 就是端口号!
这个端口号, 其实是一个两个字节的非负整数。 取值是0 ~ 65535。 它和ip地址不同, ip地址唯一标识一台全网的主机。 端口号则唯一标识某台主机里面唯一的一个网络进程。所以, 端口号和ip类似——ip是全网唯一, 而端口号是主机内唯一。所以在一个主机内一个端口号只能被一个进程绑定。即:端口号port,用来标识该主机上唯一的一个进程!
socket
端口号大概被客户端怎么使用的如下:
- 这个端口号, 商家在做客户端的时候, 就将服务端的端口号写到了客户端里面, 所以客户端天然就知道服务端的端口号是什么。
- 当客户端传送数据的时候, 就将服务端的端口号以及自己的端口号当作报文封装起来, 交给传输层。 传输层封装自己的报文后,再向下交付。 依次按流程执行, 当报文交付到服务端的传输层的时候, 传输层解开报文后, 拿到报头和有效载荷, 就能得到端口号, 然后根据端口号将有效载荷交给对应的应用,而这个应用, 必然是服务端!
- 然后, 我们知道, 客户端在封装报文时将自己的端口号也封装进去了, 所以服务端再将信息传回的时候,就能知道客户端的端口号是什么。 所以服务端就将客户端的端口号封装成报文, 依次交付直到交付到源主机的传输层。 传输层拿到后就能分离有效载荷和报头, 就能得知客户端的端口号, 就将有效载荷给客户端了!
所以, 由上面我们就能够发现, 客户端和服务端都是用自身的ip和端口号。 ip + 端口号就能表示全网当中唯二的两个不同的进程。 所以我们进程间通信的时候我们就能够互相表示对方的唯一性了!!!——这种基于ip + 端口号的通信方式, 我们把它叫做socket。
端口号 VS 进程PID
我们可能会有疑问, PID已经能够表示进程唯一性了, 为什么还要有端口号呢?有两点原因, 如下:
1、不是所有的进程都要网络通信, 但是所有的进程都要有pid。 (这句话是在告诉我们网络的功能我们需要单独设计。)
2、为了让系统功能和网络功能更好的去解耦。
绑定端口号
我们说每一个进程都有一个端口号, 传输层根据端口号找到对应的进程。 问题是, 这里面的细节是什么, 传输层怎么查找到的对应的进程。
是因为, 我们的操作系统内部都会给我们生成一张哈希表。 哈希表里面的每一个元素都是进程PCB的指针。 在哈希表中, 我们的进程将端口号作为key, 自己的PCB指针作为value, 映射到哈希表中的一个位置上面。 ——这就是绑定。 未来传输层查找某一个进程的时候, 直接利用端口号作为key查找进程的PCB指针。 就能在O(1)的时间复杂度内快速找到对应的进程。
ps:一个进程可以绑定多个端口号, 但是一个端口号不可以被多个进程绑定。
udp和tcp
tcp——传输控制协议
- 保证可靠性
- 面向连接
- 面向字节流
udp——用户数据报协议
- 无连接
- 不可靠传输
- 面向数据报
ps:技术层面上可靠不可靠是一个中性词, 没有褒贬。 就比如化学里面的惰性气体。 因为可靠是要有很大成本的, 意味着设计更复杂, 时间成本更大, 维护更不简单。 所以, 可靠与不可靠是一个中性词。
网络字节序列
我们已经知道, 内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据同样有大端小端之分。 那么如何定义网络数据流的大小端呢?
网络规定数据流必须是大端。 当主机发送数据流的时候如果是小端, 就先将数据转化成大端, 否则就忽略直接发送即可。 而规定的这个大端,其实就是TCP/IP规定的网络字节序!
套接字编程的种类
套接字编程的种类有三种:
- 1、域间套接字编程——》同一个机器内的进程间通信(本地通信)。
- 2、原始套接字编程——》绕过传输层, 直接访问网络层, 用来编写网络工具。
- 3、网络套接字编程——》用户的网络通信。
网络接口的设计者想要将网络接口统一。 保证参数的类型是统一的。 所以,我们的套接字类型就统一使用了struct sockaddr类型。 如下图:
其实, 本质上, 我们的网络套接字使用的是:
我们的域间套接字本质上是:
如何做到这种情况呢, 16位比特位置之后的数字它其实不管, 关键是我们会发现无论是网络通信的这个sockaddr_in还是域间套接字的这个sockaddr_un, 他前16位比特位都是表示的类型。 一个叫做AF_INET, 一个叫做AF_UNIX。 所以, 我们sockaddr的前16个比特位其实也是一种类型。
所以, 未来我们使用接口, 比如bind函数, 它在他的内部实现的时候。 他其实可以对内部进行判断的。 因为我们的整个接口, 每一个结构体的前两个字节它里面就包含了AF_INET或者AF_UNIX的字段。 下面是伪代码:
if (address->type == AF_INET)
{
//走网络的代码
}
else
{
//走域间的代码
}
套接字接口
socket
这里面的第一个参数就是表示将来创建套接字的域, 这个域就是表示未来我们使用的是ipv4还是ipv6或者域间套接字。 ——即,表示套接字的类型。
第二个参数表示当前socket的类型, 第三个参数表示tcb还是udp, 我们一般不用填, 这个一般填零就可以了。 一般前两个参数就已经能够很好的表示到底是udp还是tcp了。
这张表就是套接字的域选项, 比如说有AF_INET表示ipv4, AF_UNIX表示域间套接字等等。
然后这是第二个参数的套接字类型选项, 比如说是面向字节流还是面向数据报
返回值就是一个新的套接字被返回, 否则就是-1被返回, 错误码被设置。
所以, 套接字的本质其实就相当于在底层打开了一个文件, 只不过以前的struct file指向的是具体的磁盘, 键盘灯设备。 而这个套接字指向的是底层的网卡设备。
bind
bind是用来绑定套接字的。第一个参数就是socket返回的值, 也就是网卡的pid。 然后第二个参数就是套接字结构体。
返回值就是成功零被返回, 失败-1被返回, 错误码被设置。
recvfrom
上面的第一, 二, 三个参数不解释。 下面我们看第四个参数flags。 flags就类似于我们的waitpid里面的阻塞方式。 默认为0, 就是阻塞。 味蕾我们要受到一个消息, 我们是不是还得知道这个消息是谁给我们发的。 因为我们将来可能要给别人回消息。
谁给我发的就有最后两个参数来定。 最后两个参数其实是一个输出型参数。(最后的参数也是一个输入性参数。)什么意思, 将来我们如果要别人来访问我们, 那么他就会把对端的套接字信息保存到给指针指向的内存空间里面。 将来呢, 这个src_addr是一个缓冲区, 而这个addrlen就是它的长度。 (实际上我们这里传入的还是struct sockaddr_in)
ps: udp没办法使用read和write, read和write是给面向字节流的文件使用的。 而我们的udp是一个数据报, 所以我们只能使用recvfrom接口, 这个接口就是收一个消息从一个套接字中。 从指定的套接字中得到指定的报文。
sendto
sendto接口的参数,第一个就是, 第二个就是发送的字符串和长度。 第三个flags设为零。最后的这两个参数是两个输入性参数。 今天我要把消息发回给对方, 我现在知不知道我现在的消息应该发回给谁? ——我们知道, 所以我们这两个参数就是发回给的那个主机的sockaddr。
sockaddr
我们看一下sockaddr的定义:
这里面的in_port_t和in_addr其实都是整形:
然后SOCKADDR_COMMON是一个宏。 这个宏里面传的是sin_, 转到宏定义其实就知道是将sa_family_t定义为sin_famliy
然后sa_family_t就是
可以看到, 也是个整形。
ip地址的整数与字符串转化
整数如何转化成字符串
ip地址是:[].[].[].[], 每个区域是0 ~ 255。 我们可以定义一个结构体:
struct IP
{
uint8_t part1;
uint8_t part2;
uint8_t part3;
uint8_t part4;
}//字节数为4;
假如有一个整数int src_ip = 123456789.收到这个整数之后, 就定义一个struct IP* p = (struct IP*)src_ip;
让然后转一下就可以了:to_string(p->part1) + "." + to_string(p->part2) + "." + to_string(p->part3) + "." + to_string(p->part4);
如何字符串转整数
假如 "192.168.50.100", 就先利用字符串分割。 以"."为分割符, 切割后——》"192", "168", "50", "100"。
然后定义uint32_t IP;
struct ip* x = (struct ipt*)&IP;
x->part1 = stoi("192");
x->part2 = stoi("168");
x->part3 = stoi("50");
x->part4 = stoi("100");
ps:我们的ip地址和端口号都必须是在双方的报文中携带的。 否则对方发送回来的时候无法得知源主机的ip地址。 如果ip要被网络使用, 除了要把它转化为4字节ip, ip地址也必须是网络序列的。 而转化的时候我们就可以根据主机是大端还是小端来控制上面四个part的赋值顺序。
htons
其实, 我们在将主机上面的大小端转化为网络字节序列的时候, 不需要自己手动去做。 可以使用htons接口, 这个接口就是将主机字节序列转化为网络字节序列:
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!