【linux】网络套接字编程(一)端口号port,认识UDP协议和TCP协议,网络套接字,socket,bind
·
小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系统编程专栏<—请点击
linux网络编程专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
前言
【linux】网络基础(一)认识协议,网络协议,网络协议分层框架搭建,网络传输基本流程,跨网络的数据传输——书接上文 详情请点击<——,本文会在上文的基础上进行讲解,所以对上文不了解的读者友友请点击前方的蓝字链接进行学习
本文由小编为大家介绍——【linux】网络套接字编程(一)端口号port,认识UDP协议和TCP协议,网络套接字,socket,bind
一、理解网络通信的本质
- 我们在进行通信的时候,是不是两太机器在进行通信呢?原理上是,但是本质上并不是

- 当你使用左侧主机进行网络通信的时候,你使用的是主机上的客户端,例如,你使用的是抖音短视频,那么当你打开抖音短视频,那么抖音短视频一旦启动,加载到内存,就要在操作系统中变成进程运行,于是你就开始刷视频了,那么最初的抖音短视频是没有数据的,所以抖音短视频就要通过网络协议栈向远端的抖音服务器发送数据访问请求
- 而处于右侧主机上的抖音服务器本质上还是进程,服务器是一个基于while死循环的被动式的接收请求,然后做出响应。所以右侧的服务器一旦运行起来,那么在右侧主机上加载到内存,在操作系统中同样是要以进程的形式存在,所以左侧的进程通过网络协议栈发送到网络中的数据请求最终就被右侧的进程通过网络协议栈拿到了
- 所以我们日常网络通信的本质就是进程间通信。进程间通信的前提要求是让两个进程看到同一份资源,在无论是管道,共享内存还是信号量都可以保障两个通信的进程看到同一份资源,而进程通过网络协议栈接收或者发送数据到网络,这本质就是让两个进程看到同一份网络资源,即网络通信基于网络协议栈同样可以让两个进行通信的进程看到同一份资源。所以无论是网络通信,管道,共享内存,还是信号量保障进程间通信本质都是让两个进程看到同一份资源,只是各自的手段不同罢了
- 所以此时我们回头来看先忽略物理层,那么四层网络协议栈模型自上到下分为应用层,传输层,网络层,数据链路层。所以网络协议栈的下三层传输层,网络层,数据链路层主要解决的是将数据安全可靠的送到远端机器上。用户使用应用层软件,进行数据的接收和发送。那么对于这个应用层软件的应用来讲,应用一旦启动起来,那么就变成了进程了,所以网络通信的本质是进程间通信
二、端口号
- 那么我们进一步继续向后理解一下什么是端口号

- 如上我们使用左侧主机上的微信客户端想要发送信息数据到其它用户上,那么首先就要先将数据发送给微信服务端,通过微信服务端(服务器)将信息数据发送给其它用户的微信客户端上,让发送成功之后,微信服务端要给原发送数据的客户端进行网络通信回显一个回馈信息
- 所以这里我们忽略微信服务端将信息数据发送给其它用户的微信客户端上,默认发送成功,这里进行简化,只研究左侧主机上的微信客户端将数据发送给了右侧微信服务端,然后右侧微信服务端将数据发送的结果反馈给左侧的微信客户端
- 在学习端口号之前,我们已经有了IP地址的概念,IP地址可以标识主机的唯一性,将数据准确的送到主机上,即此时左侧主机上的微信客户端通过网络协议栈,会封装源IP地址(左侧主机),目的IP地址(右侧主机),所以就通过网络协议栈将数据发送到网络中,最后被右侧的主机收到了,可是问题来了,到了网络层,目的IP地址和自己的IP地址进行对比,可以确定这个信息是发送给自己这台机器的,那么网络层将数据报(报文)发送给传输层,那么在传输层将数据段解包为报文和有效载荷,但是假设传输层上层的应用层有很多应用,即微信服务端服务器,抖音服务器,网易云服务器等,那么在传输层如何确定究竟是发送给上层应用层的哪一个应用呢,如何定位应用呢?

- 所以此时端口号就登场了,端口号是一个在传输层协议(port)的内容,端口号的大小是2字节,即16个比特位的一个整数。端口号用于标识一台主机上网络应用层的一个进程。使用方法是将端口号和进程进行绑定
- 所以此时右侧主机已经早早的将服务器和端口号进行了绑定,而在当前传输层接收的报文(数据段)解包后的报头中有携带的端口号字段,即源端口号,目的端口号,其中目的端口号就和右侧主机上的微信服务器进程绑定是端口号相同,所以此时传输层通过端口号就可以将有效载荷向上分用,递达到应用层的具体的某一个应用了,即递达给上层的微信服务端
- 所以此时右侧主机上的微信服务端(进程)拿到了左侧主机上微信客户端(进程)发来的数据了,所以右侧主机将数据进行处理后,将发送结果,例如是否发送成功,发送时间,对方是否已读等信息通过网络协议栈发送给原发送数据的左侧主机上的客户端,那么问题来了,右侧主机的微信服务端如何得知给它发送数据的主机,以及主机上发送数据的微信服务端的呢?
- 很简单,通过对方发来的报文字段中就包含了对方主机的IP地址,以及对方主机上的应用层中的微信客户端绑定的端口号,因为对方的主机上的微信客户端启动运行后也是进程,进行网络通信发送数据就要绑定端口号以便对方可以接收到端口号字段,未来对方想要发送消息给自己的时候,就可以通过端口号加上IP地址找到自己这台主机上的和端口号绑定的应用了
- 所以此时右侧的微信服务端就从左侧主机的微信客户端发来的报文数据中,获得了IP地址以及端口号,其中IP地址可以标识唯一的一台主机,端口号可以标识主机上唯一的一个应用(进程),所以此时右侧的微信服务端就将自己的服务端所在的主机的IP地址作为源IP地址,服务端绑定的端口号作为源端口号,将对方主机的IP地址作为目的IP地址,对方主机上的应用层的微信客户端所绑定的端口号作为目的端口号,然后通过网络协议栈就可以发送到网络中了,所以此时右侧的主机上的微信客户端通过网络协议栈类似的过程,然后就可以获得左侧主机的微信服务端发来的数据,即发送结果,并且将发送结果显示到本地的微信客户端
- 所以端口号无论对于左侧的客户端client还是右侧的服务端server,都能标识该主机上唯一的处于网络应用层的一个进程
- 所以在公网上,IP地址可以标识全网唯一的一台主机,端口号port,用来标识该主机上唯一的一个进程 。所以IP : port = 可以标识全网唯一的一个进程
- 而套接字socket中就可以包含IP以及port这样的字段,所以客户端client_ip : client_port和服务端server_ip : server_port就可以通过套接字socket进行网络通信
- 那么进程pid同样也可以标识一台主机上进程的唯一性了呀,为什么在网络中还要引入端口号这样的内容呢?
(1)首先注意一点,不是所有的进程都要网络通信,但是所有的进程都要有pid
(2)进程pid是系统方面的概念,如果网络模块也采用系统方面的进程pid,那么如果未来系统的pid改了,那么网络是不是也要跟着修改,并且网络和系统是操作系统中两个不同的模块,我们不期望网络和系统之间有强耦合的关系,而是希望两个模块之间尽量的降低耦合度,所以在网络模块才设计的自己的用于标识主机中唯一的一个进程的端口号,这样你系统模块就算修改了进程pid,我网络模块也不需要进行修改,而是继续采用我网络模块原来的端口号即可,这样就是实现了网络功能和系统功能在一定程度上的解耦合
三、端口号的绑定
- 那么既然主机上的一个进程可以绑定端口号,那么究竟如何给一个进程绑定端口号,并且在传输层又是如何通过端口号找到唯一的一个进程的呢?

- 那么我们以用户使用抖音短视频为例进行讲解,那么我们在左侧主机最初启动打开抖音短视频的时候,那么在操作系统中抖音短视频就要变成一个进程,所以在应用层就有了抖音短视频这个进程,即此时用户发起了想要获取远端抖音服务器上的多个抖音短视频的视频,所以在网络协议栈中假设抖音短视频已经在传输层绑定了4321这个端口号,并且在网络层获取了自己主机的IP地址,并且层层向下封装报头形成报文数据,所以此时抖音短视频就要通过网络协议栈进行网络通信向远端的抖音服务器发送获取的数据请求
- 所以此时远端的抖音服务器所在的主机就收到了这个报文,那么经过解包分用,报文到了传输层,此时传输层将数据短(报文)进行解包为报头和有效载荷,在报头中,拿到了目的端口号8080,所以也就意味着抖音服务器绑定的端口号是8080,问题来了,进程如何进行的绑定端口号?
- 首先,在传输层会建立一个哈希表,关于哈希表的介绍,详情请点击<——,有了这个哈希表之后,如果进程想要绑定端口号,例如,抖音服务器这个进程想要绑定端口号8080,那么就会将这个端口号8080使用哈希函数进行运算,找到对应的在哈希表的位置,如果这个位置已经被进程绑定了,那么就会向上返回绑定失败,因为一个端口号只能绑定一个进程,如果这个位置没有被进程绑定,在哈希表中存储的是进程PCB的指针,所以就会将抖音服务器进程的PCB的指针放到对应的位置中,这样就完成了进程对应端口号的绑定
- 所以从此以后,如果在传输层,拿着端口号8080使用和绑定端口号的同一个哈希函数,那么进行运算后得出的位置一定是同一个位置,即找到了抖音服务器PCB的指针,所以此时就找到了抖音服务器了,所以抖音服务器就知道了在远端有一个抖音客户端想要获取短视频数据,报文中有IP地址以及端口号字段,所以就可以从源IP地址知道是哪一台主机发来的请求,从源端口号知道是哪一个进程发来的数据请求,那么此时抖音服务器就会将抖音短视频的视频数据进行打包,向下进程逐层的封装,将目的IP地址封装成对方进程的IP地址,将目的端口号封装成对方进程的端口号4321,源IP地址封装成自身的IP地址,源端口号封装成自身的端口号8080
- 所以此时抖音服务器对应的主机就将数据包(报文)通过网络协议栈发回给了左侧的主机上的抖音客户端,所以此时用户就可以开心愉快的浏览短视频了,同样的在客户端的这个抖音进程也绑定了端口号,并且在传输层中也有对应的哈希表,通过哈希函数将抖音进程绑定端口号建立哈希映射,所以就可以拿着端口号4321进行哈希函数的计算,在哈希表的对应位置找到抖音进程的PCB,才能找到并访问抖音客户端,将短视频数据同步到抖音客户端中,用户才能愉快的浏览短视频
- 那么问题来了,服务器是一个基于while死循环的被动式的接收请求,然后做出响应,即服务器不会主动向客户端发送数据,但是客户端会主动向服务器发送数据请求吗?
- 当然会了,客户端代表的就是用户,用户每次开始刷抖音短视频的时候,这些视频不是平白无故就有的,这些视频都是从远端的抖音服务器上将视频数据传输回来的,当你打开抖音短视频的时候,抖音短视频这个客户端就要向远端的抖音服务器发送数据请求了,那么问题又来了,进行网络通信,客户端知道自己的IP地址,知道自己绑定的端口号,但是客户端如何得知服务器的IP地址,如何得知服务器的端口号呢?

- 很简单,例如王者荣耀客户端,王者荣耀服务器都是腾讯这家公司开发的,那么在王者荣耀客户端中腾讯一定会将王者荣耀服务器的IP地址(间接内置)和端口号(直接内置)内置在了王者荣耀客户端中。当你在手机软件商店中找到下载王者荣耀客户端的时候,当你下载成功后,启动运行王者荣耀客户端,王者荣耀客户端就会变成一个进程运行,在登录的时候,即此时用户使用王者荣耀客户端向远端发送登录的数据请求,所以王者荣耀客户端就可以直接拿着内置的王者荣耀服务器的IP地址(间接内置)和端口号(直接内置)通过网络协议栈向远端王者荣耀服务器发送登录的数据请求
- 所以每一个服务器的端口号一定是被精心设计的,并且广为人知的,即一定要被客户端知道
- 那么下面小编有两个问题考一考大家,一个进程可以绑定多个端口号吗?一个端口号可以被多个进程绑定吗?
- 答案依次是能,不能
(1)能,因为一个进程可以绑定多个端口号,那么拿着端口号在传输层的的哈希表中进行哈希函数的运算会得到唯一的一个位置,所以此时进行访问可以访问到唯一的一个进程,即端口号的目的就是标定主机中唯一的一个进程,此时无误
(2)不能,但是如果一个端口号被多个进程绑定,那么拿着端口号在传输层的的哈希表中进行哈希函数的运算会得到多个位置,即此时一个端口号就对应多个进程,所以此时就无法确定究竟是要访问哪一个进程了,即使用这样的端口号是无法标定主机中唯一的一个进程,所以不能让一个端口号被多个进程绑定
四、认识UDP协议和TCP协议
- 在传输层中的协议以UDP协议和TCP协议为代表,在TCP协议和UDP协议的字段中有两个端口号,分别是源端口号和目的端口号,用来表示数据是谁发来的,要发给谁的
认识TCP协议
- TCP协议(Transmission Control Protocol传输控制协议),下面是TCP协议的一些特点
(1)传输层协议
(2)有连接
(3)可靠传输
(4)面向字节流 - TCP协议是一个传输层协议,当使用TCP协议进行数据传输网络通信的时候,TCP协议要和通信主机建立连接,并且保证数据真的被发到了对方的主机上,并且保证数据不丢失,TCP协议是基于字节流发送数据的,即将数据基于一个字节一个字节的发送给对方主机
- TCP协议由于其安全可靠,所以在一些支付领域,安全方面使用较多,例如支付宝,银行,要使用TCP协议保证安全性
- 同样的,我们也应该看到TCP协议的另一面,效率相对较低,由于使用TCP协议,首先就要建立连接,连接的建立必然要花费时间,并且TCP协议要保证数据不丢失,那么也就意味,当TCP没有收到对方发来的确认收到的数据的信息之前,当TCP把数据通过网络协议栈发到网络中的时候,TCP不会将数据直接丢弃,而是将数据暂时保存起来,不丢弃数据,暂时保存数据也是一种对系统的消耗
- 并且在数据发送到网络中的时候,TCP还要时刻进行数据检测算法,时刻检测也是要花费时间的,一旦检测到数据丢失或者数据乱序,还要重新发送数据,或者调整数据顺序并且重新发送数据
- 由此可见TCP协议为了保证可靠,安全传输做了很多的工作,那么也就必然决定了TCP协议的效率相对较低
认识UDP协议
- UDP协议(User Datagram Protocol用户数据报协议),下面是UDP协议的一些特点
(1)传输层协议
(2)无连接
(3)不可靠传输
(4)面向数据报 - UDP协议是一个传输层协议,UDP使用网络协议栈进行网络通信,向对方主机发送数据的时候,不会和对方主机建立连接,UDP协议是一个不可靠传输,即UDP协议一旦将数据发送出去,那么在本地就会立即将数据丢失(释放内存资源,效率高),不会去做数据检测,并且不会再去关心据究竟有没有丢失,数据有没有乱序,数据有没有发送到对方主机上,并且我们还可以看出UDP协议不去做这些可靠性,安全性的工作,那么在一定程度上UDP协议是效率高的,低延迟的,并且UDP协议是比TCP协议快的
- UDP协议是面向数据报的,即直接一次性的将数据全部发送给对方主机
- UDP协议的应用场景也有很多,例如电子邮件,音视频通信,网络监控和日志传输等
- TCP协议和UDP协议没有谁好谁坏之分,所谓的特点仅仅是对应协议的特征,我们在实际的应用场景中要理性的使用TCP协议和UDP协议
五、网络字节序
- 我们知道数据在内存中存储分为大端存储和小端存储,所以如果一个主机是大端机,另一个主机是小端机,那么两个主机之间直接进行网络数据通信,那么就会出现数据错乱的情况,并且就算在数据的字段中带上究竟是大端机器还是小端机器,但是如果当前主机是大端机收到数据是小端机的,尽管小端机的数据中带了字段用于标识当前数据是大端机还是小端机,但是接收方是大端机,大端机看不懂小端机发来的数据,所以自然也就无法从小端机发来的数据的字段中得出究竟对方是大端机还是小端机了,那么如何处理呢?
- 很简单,我网络直接规定,从此以后,无论你是什么机器,只要使用网络进行通信,那么发送到网络中的数据一律都要采用大端机器的存储序列,所以这样就巧妙的解决了数据因为大端机器或者小端机器的存储数据的方式直接向网络中发送数据而造成的数据混乱的情况
- 那么什么是大端存储方式,即将数据的高位放到内存序列的低地址处,将数据的低位放到内存序列的高地址处
- 那么什么是小端存储方式,即将数据的低位放到内存序列的低地址处,将数据的高位放到内存序列的高地址处
- 如何记忆?这里我们仅仅记忆小端存储即可,即小小小,即数据的低位(小)放到内存序列的低地址处(小)就是小端(小),大大大,将数据的高位(高)放到内存序列的高地址处(高)的就是小端(大),其余情况都是大端,所以这里我们仅需要记忆小小小即可
- 同样的在网络字节序中还有以下规则
(1)发送主机通常发送缓冲区中的数据,按照内存地址从低到高的顺序发送
(2)接收主机把从网络中接收到的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序保存
(3)因此,网络字节流的顺序中,先发出的数据是低地址,后发出的数据是高地址
(4)TCP/IP协议规定,网络字节流应该采用大端字节序,不能采用小端字节序
(5)因此,不论当前主机是大端机还是小端机,都需要按照TCP/IP协议规定的网络字节序来发送/接收数据
(6)所以如果当前主机是大端机,那么直接向网络中发送数据即可,如果当前主机是小端机,那么需要先转换成网络字节序,即将小端字节序转换成大端字节序之后再向网络中发送数据 - 那么如何将当前字节序进行转换为网络字节序呢?使用如下函数接口即可

(1)uint32_t是32个比特位,即4个字节,一个int类型,那么对应的就是IP地址的大小,uint16_t是16个比特位,即2个字节,一个short类型,那么对应的就是端口号的大小
(2)关于函数的命名很好理解,h是指当前主机,to是指转化,n是指网络,s是指短整数short(即对应的就是端口号),l是指相较于short的长点的整数int(即对应的就是IP地址)
(3)例如htonl,那么就是将主机传入的字节序列转换为网络字节序列,如果当前主机是大端机,即大端字节序,那么htonl将会直接返回这个大端字节序,如果当前主机是小端机,即小端字节序,那么htonl会将传入的序列转换为大端序列然后返回这个大端序列
六、套接字
套接字的种类
- 套接字是包含多种数据类型
(1)域间套接字,用于同一个主机内编程的使用,原理是使用文件路径进行标识,看到同一个文件
(2)原始套接字,通常用于在网络协议栈中,跨过传输层,直接封装或调用网络层,数据链路层的系统调用接口,通常是用于编写一些网络工具,例如抓包工具等
(3)网络套接字,用户间的网络通信,即跨主机的在网络中进行通信的要使用到的数据类型,基于网络套接字进行网络编程 - 那么这三种套接字的对应的套接字编程所使用的函数接口应该要有三套,可是三套的学习成本太大了,所以网络接口的设计者设计了一套函数接口直接就可以间接的使用这三种网络套接字,那么三种套接字使用同一套函数,首要要解决的就是函数接口的参数要相同,那么如何才能做到函数的接口相同呢?以下面的创建套接字socket,和套接字的绑定bind为例进行讲解
socket

- socket用于创建套接字,其中可以设置不同的参数,第一个参数可以选择通信的域,即协议家族,选择是本主机范围进行通信,即本主机上的两个进程通信(使用AF_UNIX或AF_LOCAL)还是使用网络进行通信(使用AF_INET或者AF_INET6),第二个参数type即选择协议类型,我们主要关注的是创建网络套接字,网络套接字的类型中有UDP协议的网络套接字和TCP协议的网络套接字
- 其中UDP协议和TCP协议基于IPv4进行通信,那么第一个参数就要设置为AF_INET(如果是IPv6,那么就要设置为AF_INET6),UDP协议是数据报方式发送数据到网络,所以如果要选择UDP协议,那么socket的第二个参数设置就要设置为SOCK_DGRAM,而TCP协议是以字节流方式发送数据到网络,所以如果要选择TCP协议,那么socket的第二个参数设置就要设置SOCK_STREAM,第三个参数默认都设置为0即可
- socket的返回值是一个文件描述符int类型,这里我们创建的是有关网络的套接字,所以返回的即网络文件描述符,即这个文件描述指向一个套接字对象,那么也就意味着从此以后,我们想要对套接字进行操作,那么一定要传输这个socket的返回值,同样这也符合linux下一切皆文件的特点
bind

- bind是对套接字的进行绑定,第一个参数果然如我们所料,要使用套接字的文件描述符,第二个参数传入一个struct sockaddr的结构体,第三个参数传入这个结构体的大小
- 那么在网络套接字中,则是绑定端口号,绑定IP地址,可是在套接字类型中,可不仅仅有网络套接字,还有域间套接字,那么如何设置呢?使用如下结构体即可

- struct sockaddr_in用于设置网络套接字的属性,包括端口号,IP地址,其中前16个字节的字段需要被设置
- struct socketaddr_un用于设置域间套接字的属性,包括文件路径,其中前16个字节的字段需要被设置
- 我们观察同样的struct socketaddr的前16个字节的字段同样也需要被设置,这三个结构体的前16个字节的字段都需要被设置,难道这是巧合吗?
- 不是的,为了使用同一的接口bind,但是不同的套接字类型需要设置的参数类型不同,可是使用的却是同一个接口函数bind进行设置,那么也就意味着形参却又必须相同,可是不同套接字类型需要设置的参数类型不同,这可如何是好?
- 很简单,那么你网络套接字将需要设置的属性在本地定义一个struct sockaddr_in设置好端口号,IP地址,前16个字节设置为AF_INET,域间套接字在本地定义一个struct socketaddr_un结构体,设置好文件路径,前16个字节设置为AF_UNIX,所以如果你想要设置不同的套接字属性,在本地定义对应的类型,将参数属性字段设置好,那么此时将套接字属性设置好了吗?
- 没有,因为当前定义的结构体是在当前用户栈上定义的,即用户区,而网络模块是被写到操作系统内部的,即内核区,所以当前设置的结构体的对应属性还没有设置进内核,那么如何设置呢?
- 很简单,使用bind进行设置即可,小编,小编,可是这两个结构体的类型都不同,你叫我如何传参呢?所以需要一个中间桥梁,bind的第二个参数的类型是struct socketaddr*,那么我们将本地的struct sockaddr_in或struct socketaddr_un取地址,然后强制类型转换为struct socketaddr*,这三个结构体的前16字节的字段都是相同的,所以在bind内部就可以根据前16个字节进行判断了
if(address->type == AF_INET)
{
//将struct socketaddr\*强转为struct sockaddr_in*
//把传入的结构体当作网络套接字模块功能的设置
}
else if(address->type == AF_UNIX)
{
//将struct socketaddr\*强转为struct sockaddr_un*
//把传入的结构体当作域间套接字模块功能的设置
}
- 所以就可以使用同一个接口设置不同的套接字属性了,那么还有一个问题,小编,小编,一般我们设置通用类型的时候都是使用的void*,那么这里使用void*不是更好吗?
- 没错,但是网络这一块被编写出来的时候,c语言中还没有void*,所以自然没有办法使用void*,代码通常来讲都是向前兼容的,因为当你这个网络模块被编写出来后,有很多人已经使用了网络模块的代码进行编程,一旦修改成了void*,那么人们编写的代码都要跟着修改,要知道这可是会引起公愤的,诸如c++中的auto_ptr虽然写的比较挫,存在者被拷贝对象悬空,访问被拷贝对象存在空指针的访问的问题,但是c++中的auto_ptr也是没有修改的,所以linux中也一样不能修改
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!
更多推荐



所有评论(0)