网络基础
- 了解网络发展背景, 对局域网/广域网的概念有基本认识;
- 了解网络协议的意义, 重点理解TCP/IP五层结构模型;
- 学习网络传输的基本流程, 理解封装和分用;
网络编程套接字
- 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
- 学习socket api的基本用法;
- 能够实现一个简单的udp客户端/服务器;
- 能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本);
- 理解tcp服务器建立连接, 发送数据, 断开连接的流程
网络发展背景
- 光猫—拨号上网,进行信号的转换
协议
- 协议是沟通双方约定采用同一种语言进行有效的沟通**(这只是通俗的协议的理解)**
- 网络协议:指的是网络当中通信双方,采用同一种数据格式来进行有效的通信(在网络中其实是由很多网络协议的,下面引出网络协议簇)
- 网络协议簇:在网络当中不止有一个协议,而是有很多个协议,多个协议称为协议簇
- 体系结构/参考模型:典型的有两种,一种是OSI参考模型,另一种是TCP/IP参考模型,体系结构/参考模型其实就是—它规定了协议完成的任务以及协议的分层(或者协议要支撑的上下层)
OSI参考模型(物数网传会表应)
-
分层最大的好处在于 "封装"
-
物数网传会表应对应的是7个层,分别为物理层,数据链路层,网络层,传输层,会话层,表示层,应用层(分层太细了,所以其实是并不利于开发的,所以提出了TCP/IP模型)
-
OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范;
-
把网络从逻辑上分为了7层. 每一层都有相关、相对应的物理设备,比如路由器,交换机;
-
OSI 七层模型是一种框架性的设计方法,其最主要的功能使就是帮助不同类型的主机实现数据传输;
-
它的最大优点是将服务、接口和协议这三个概念明确地区分开来,概念清楚,理论也比较完整. 通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯;
-
但是, 它既复杂又不实用; 所以我们按照TCP/IP四层模型来讲解
-
表示层:—主要功能是数据格式化,代码转换,数据加密,表示层是没有协议的,表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别,如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一
-
会话层—主要功能是解除或建立与别的接点的联系 ,会话层也是没有协议的,会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成
TCP/IP参考模型
- 可以称之为五层,也可以称之为四层,分别为:物理层,数据链路层,网络层,传输层,应用层(五层和四层的区别就是到底算不算入物理层的问题)
TCP/IP参考模型(具体)
-
应用层(目前所写的所有的代码都是属于应用层的代码,也就是说我们程序员是在这一层工作的):典型的协议:http协议(超文本传输协议),DNS协议(DNS是域名系统),DHCP协议(这些协议是应用层典型的协议),应用层的功能其实是—文件传输,电子邮件,文件服务,虚拟终端,以及为操作系统或网络应用程序提供访问网络服务的接口**(为应用程序提供服务)**
-
传输层:传输层所要完成的功能其实就是点对点传输,点的其实就是我们所说的端点,或者说是我们所说的端口,典型的协议:TCP协议,UDP协议(当所要传递的数据到达传输层的时候,会在传输层加上包头,包头中包含有一个非常重要的东西叫做端口,如果使用的是TCP协议,就打上TCP的包头,如果使用UDP的协议,就打上UDP的包头),传输层只关心端口,并不关心ip地址,接收上一层传输的数据,在必要的时候对数据进行分割的操作,并把这些数据交给网络层,并且保证这些数据有效达到对端,传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。
-
网络层:负责路由转发以及地址管理(为数据包选择路由),典型的协议:IP协议,典型的设备:路由器(等网路层拿到数据之后,他会给出自己的包头,其实也就是打上了IP协议的包头,里面同样包含一个非常重要的概念,这个非常重要的概念,被称之为IP地址,在这里的IP只关心自己从哪里来,会到那里去的问题, 本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础
-
数据链路层:负责相邻的设备的传输,典型的协议:以太网协议,典型的设备:交换机设备,以及网桥(数据链路层同样会加上自己的包头,他要加上两个部分,一个是头(中包含了mac地址),一个是尾) 将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测
-
物理层:负责广电信号的传输,典型的协议:以太网协议,典型的设备:集线器(我们数据过来的时候其实是一堆二进制的,等到数据过来的时候,物理层就会把一堆二进制转化为光电信号,光电信号说的再直白一些其实就是高低电频,在这一层,我们所传数的二进制信号会变成光电协议,集线器的作用是放大我们现在所传输的信号,因为我们信号在传输的过程中存在一个信号衰弱的问题,当我们的信号衰弱的时候,集线器就会对我们传输的信号进行放大的操作,放大之后才能达到长距离传输的目的,否则是无法进行长距离传输的,物理层其实就是以二进制的形式在物理媒体上传输数据
-
物理层的数据传输单位是比特,数据链路层数据单位是帧,网络层的数据单位是数据包,传输层的数据单元也称作数据包
网络传输当中数据的五元组信息
- 就比如说我们现在在收发快递的时候,肯定要有收件人和收件人地址,同时还存在有寄件人和寄件人地址,以及这个快递是那一家快递公司的,这五部分信息对应到网络当中分别是,收件人地址相当于对应的是目的ip地址,收件人可以理解为目的端口,寄件人可以看为源端口,寄件人地址可以堪为源ip地址,那一家公司的快递就可以看为协议
- 所以五元组信息其实就是源ip,源端口,目的ip,目的端口,以及协议
- ip地址:ip地址在网络中唯一标识一台主机
- 端口(port)一个端口在一台机器中唯一标识一个进程,一个进程可以占用多个端口
- MAC地址其实在决定当前我的这个数据要到达的下一个设备到底是什么
- 传输从把数据从应用层接收到之后他就知道这个数据是从哪来的了
数据包封装和分用
- 不同的协议层对数据包有不同的称谓,在传输层叫做段(segment),在网络层叫做数据报 (datagram),在链路层叫做帧(frame).
- 应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部,称为封装
- 首部信息中包含了一些类似于首部有多长, 载荷有多长, 上层协议是什么等信息.
- 数据封装成帧后发到传输介质上,到达目的主机后每层协议再剥掉相应的首部, 根据首部中的 “上层协议字段” 将数据交给对应的上层协议处理
- 封装
- 分用
ip地址
- ip地址本质上是一个无符号的32位的整数,也就是说他的范围是(0~2^32-1),port是无符号的16位的整数
- IP协议有两个版本, IPv4和IPv6. 我们整个的课程, 凡是提到IP协议, 没有特殊说明的, 默认都是指IPv4
- 通常,我们在表示ip的时候是使用点分十进制的方式来进行表示的,以点分割,每一个数字都是占用一个字节的,如果利用点分十进制的方法来进行表示,某一个字节如果是超过了255的话其实就是不合法的,点分十进制的本质其实还就是一个无符号的整数,整个ip地址占用的字节数是四个字节(32个比特位)
- 因为ip地址的范围是(0~2^32),那么也就是说,其实最多也就只有42亿多个ip地址,那么这么多个ip地址很显然其实是并不够我们去进行使用的(ipv4版本的ip地址面临枯竭)
- DHCP协议:是动态主机分配协议,他的核心其实就是谁上网就给谁分配ip(自动获得IP地址其实就是DHCP协议),在一定程度上可以缓解ip地址枯竭的问题
- NAT协议:地址转换协议,可以真正有效的去解决ip地址枯竭的问题
- ipv6版本的ip协议—ip地址的本质上是uint128_t,所以他的范围就是(0~2^128),但是同样有一个问题就是ipv6和ipv4是不兼容的,不兼容的原因是ip地址的长度是并不相同的,一个是四个字节,一个是16个字节
网络编程套接字
字节序(大端字节序,小端字节序,主机字节序,网络字节序
tcp和udp协议的特点
udp编程流程
tcp编程流程(单执行流的编程流程,多进程的编程流程,多线程的编程流程)
字节序
- 字节序指的是CPU对内存的访问顺序,也就是说我到底是先从低地址开始访问还是说我是从高地址开始访问
- 字节序,即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。
- 小端字节序:低位存放低地址,高位存放高地址
- 大端字节序:低位存放高地址,高位存放低地址
- 计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。如果是大端字节序,先读到的就是高位字节,后读到的就是低位字节。小端字节序正好相反
- 因为我们的机器存在有大小端之分,假如说我的现在要传输内容的机器他是小端的,然后他把数据传到一个大端的机器里面去了, 那么有可能存在的一个问题就是我原本一个很小的数据一下子就变成了要给很大的数据,即使我这两个机器之间的,协议啊什么的都是一样的,但是我们这个时候如果说对数据的解读格式不一样的话,CPU对内存的存取顺序不一样的话,导致我们理解的数据就不一样,这个是时候正是因为我们的机器存在一个大小端之分,那么我们在网络传输当中就一定要规定一个字节序从而再来进行我们的传输,这个字节序就被称为网路字节序
- 主机字节序:指的是机器本身的字节序,如果是大端,那么主机字节序就是大端,如果是小端,主机字节序就是小端
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
- 网路字节序:规定网络当中传输的字节序使用大端,这也就意味着,如果是小端机器在传输数据的时候,需要将数据转换为大端字节序来进行传输,对端机器默认传输过来的数据是大端字节序的
- 网络字节序:网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题,把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的
- 通常来说,字节序和计算机的架构有关系,最常见的x_86体系架构就是小端机器
主机字节序如何转换成为网路字节序
- 那么就需要来看一下ip和端口要如何进行转换了
- 首先先看一下ip,这这个接口的功能是将主机字节序转换成为大端字节序,那么这个接口的含义其实就是有两层含义的,第一层含义就是说,假设我现在的主机是小端字节序,那么我在调用这个接口的时候我就需要将小端字节序转换成为大端字节序,如果我现在的主机是大端字节序的话,那么其实我在调用这个接口的时候,就什么都不用去做了
网路字节序如何转换成为主机字节序
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大端转换然后返回
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
#include <arpa/inet.h>
//将主机字节序转换为网络字节序
unit32_t htonl (unit32_t hostlong);
unit16_t htons (unit16_t hostshort);
//将网络字节序转换为主机字节序
unit32_t ntohl (unit32_t netlong);
unit16_t ntohs (unit16_t netshort);
//说明:h -----host;n----network ;s------short;l----long。
TCP和UDP的区别
TCP协议
-
TCP协议:面向连接,可靠传输,面向字节流
-
面向连接指的是:TCP通信双方在发送数据之前,需要先建立连接,然后才能够发送数据
-
可靠传输指的是:TCP保证传输的数据都是无差错,不丢失,不重复,按序到达对端
-
面向字节流指的是:(1)对于传输的数据之间是没有明显的数据边界的(比如说我第一次传过去了一个123,第二次传输过去了一个456,那对端在接收到数据的时候其实是没法找到我那一次接收到的是哪个数据的) (2)对于接收方而言,在可以接受数据的情况下,可以接受任意字节的数据(就是说比如说你给我传输了123456过来,我是可以决定的,比如说我决定第一次接收一个字节,第二次接收两个字节等等.)
-
补充一点:TCP提供全双工通信。允许通信双方的应用进程在任何时候都可以发送数据,因为两端都设有发送缓存和接受缓存
-
面向字节流的理解:打个比方比喻TCP,你家里有个蓄水池,你可以里面倒水,蓄水池上有个龙头,你可以通过龙头将水池里的水放出来,然后用各种各样的容器装(杯子、矿泉水瓶、锅碗瓢盆)接水。上面的例子中,往水池里倒几次水和接几次水是没有必然联系的,也就是说你可以只倒一次水,然后分10次接完。另外,水池里的水接多少就会少多少;往里面倒多少水,就会增加多少水,但是不能超过水池的容量,多出的水会溢出。
-
结合TCP的概念,水池就好比接收缓存,倒水就相当于发送数据,接水就相当于读取数据。好比你通过TCP连接给另一端发送数据,你 只调用了一次write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次write,每次10个字节,但是对方可以一次就收完。(假设数据都能到达)但是,你发送的数据量不能大于对方的接收缓存(流量控制),如果你硬是要发送过量数据,则对方的缓存满了就会把多出的数据丢弃
UDP协议
- UPD协议:无连接,不可靠,面向数据报
- 无连接指的是:UDP通信双方在发送数据之前,是不需要进行沟通的,客户端只要知道服务端的ip和端口,就可以直接发送数据了
- 不可靠指的是:不保证数据是可靠到达对端的,而且不保证数据是按序到达的
- 面向数据报指的是:UDP对于应用层和传输层数据递交的时候,都是整条数据交付的,就是全部的一次的进行传输
- 面向数据报的解释—UDP和TCP不同,发送端调用了几次write,接收端必须用相同次数的read读完。UPD是基于报文的,在接收的时候每次最多只能读取一个报文,报文和报文是不会合并的,如果缓冲区小于报文长度,则多出的部分会被丢弃。也就说,如果不指定MSG_PEEK标志,每次读取操作将消耗一个报文。
TCP和UDP协议的特点
- UDP协议特点
(1)UDP是无连接的传输层协议;
(2)UDP使用尽最大努力交付,不保证可靠交付;
(3)UDP是面向报文的,对应用层交下来的报文,不合并,不拆分,保留原报文的边界;
(4)UDP没有拥塞控制,因此即使网络出现拥塞也不会降低发送速率;
(5)UDP支持一对一 一对多 多对多的交互通信;
(6)UDP的首部开销小,只有8字节 - TCP协议的特点
(1)TCP是面向连接的运输层协议;
(2)每一条TCP连接只能有两个端点(即两个套接字),只能是点对点的;
(3)TCP提供可靠的传输服务。传送的数据无差错、不丢失、不重复、按序到达;
(4)TCP提供全双工通信。允许通信双方的应用进程在任何时候都可以发送数据,因为两端都设有发送缓存和接受缓存;
(5)面向字节流。虽然应用程序与TCP交互是一次一个大小不等的数据块,但TCP把这些数据看成一连串无结构的字节流,它不保证接收方收到的数据块和发送方发送的数据块具有对应大小关系,例如,发送方应用程序交给发送方的TCP10个数据块,但就受访的TCP可能只用了4个数据块久保收到的字节流交付给上层的应用程序,但字节流完全一样
TCP和UDP的区别
(1)TCP是可靠传输,UDP是不可靠传输;
(2)TCP面向连接,UDP无连接;
(3)TCP传输数据有序,UDP不保证数据的有序性;
(4)TCP不保存数据边界,UDP保留数据边界;
(5)TCP传输速度相对UDP较慢;
(6)TCP有流量控制和拥塞控制,UDP没有;
(7)TCP是重量级协议,UDP是轻量级协议;
(8)TCP首部较长20字节,UDP首部较短8字节;
- UDP头部结构只有8个字节
UDP的编程
- 再说UDP编程的时候,一定要分为两个部分,这两个部分分别是客户端和服务端,在进入通信之前,还需要有一个之前的准备活动,没有前期准备是无法完成UDP的网络编程
- 前期准备工作,对于服务端来说,首先要去创建套接字,创建套接字完成之后需要去绑定地址信息,绑定了地址信息之后就可以接收来自客户端发送的消息了
- 前期准备工作对于客户端来说,首先还是需要去创建套接字,创建套接字完成之后也是需要去绑定地址信息,(这里是可以绑定地址信息也可以不绑定地址信息的,但是不推荐绑定地址信息)
创建套接字接口
- 创建套接字需要用到socket接口
- 第一个参数domain表示的是地址域(地址域这个概念其实不是很好理解的),其实也就是指定网络层到底使用什么协议,那么有什么协议可以提供给我们来进行使用呢?AF_INET: 表示的是我们在网络层使用ipv4版本的ip协议;AF_INET6: 表示我们使用ipv6版本的协议,也就是说我们在去创建套接字的时候我们就已经知道了我们在网络层使用的是什么样的协议了
- 第二个参数type指的是套接字类型,这里有两种类型供我们使用,一种为SOCK_DGRAM:表示的是用户数据报套接字;SOCK_STREAM:表示的是流式套接字 引申出来的含义其实是在指定传输层使用什么样的协议,一个是UDP,一个是TCP
- 第三个参数–是协议,是建立在第二个参数的选择基础之上的
- 使用下面的这个命令进入到下面的路径中
- 然后我们其实就可以看到一个枚举,就可以看到所支持的协议
- 重点要看的是下面的两个协议
- TCP是6
- UDP是17
- 返回值—返回的是一个套接字操作句柄,这个套接字操作句柄,本质上其实就是一个文件描述符,也就是说他从文件描述符表中是一定可以看到的,因为文件描述符是没有小于0的值的,所以正常返回也是不会小于0的
创建套接字的意义(为什么一定要去创建套接字呢)
前提在于
-
一台设备要进行网络通信,一定是需要有一个网卡设备来接收数据的
-
网络当中的数据到达我们当前机器当中,他是先到达网卡设备当中的**(ens33其实就是我们当前的网卡)**,如果我们当前的设备没有网卡的话,他是无法进行网路间通信的,原因就在于我们网络中的数据是先到达网卡当中的
-
inet指的是ip地址,netmask是子网掩码,broadcast是广播地址
-
下面的这个其实是MAC地址,每一个网卡设备都是有一个MAC地址的,称之为物理地址,每一块网卡设备中的MAC地址一定是唯一的(MAC地址在数据链路层中)
-
物理地址占用6个字节,每一个冒号分割一个字节
-
那么创建套接字的意义其实就在于将进程和网卡绑定,进程可以从网卡当中接收数据,也可以通过网卡进行发送数据
绑定地址信息
-
绑定地址信息的本质其实就是绑定ip和端口,服务器既然想要提供某些服务,必须让客户端知道怎么将数据传输给服务端,而网络当中传输数据的时候,是按照ip地址和端口(port)来进行传输的
-
如果要是再去细分的话,在整个网络链路当中的传输,本质上是和端口没有任何的关系,因为在网络中传输的时候只关心IP,因为IP可以唯一的确定一台主机,只有这个数据到达了主机之后,我才去关心端口,因为端口是可以唯一标识一个进程的,那么,也就是说,在整个网络链路当中的传输,ip地址起到了作用,当数据达到目标主机之后,是根据端口号来识别数据到底是哪个进程的
-
绑定接口信息,我们需要用到bind接口
-
第一个参数,sockfd是套接字描述符/套接字句柄(是socket函数的返回值)
-
第二个参数,要告诉操作系统内核,当前进程要绑定的地址信息是什么
-
第三个参数,绑定的地址信息的长度是多少,难点在于第二个参数和第三个参数
既然难点在第二个参数和第三个参数,那么现在就去分析一下源码
- 看一下第二个结构体这个参数到底是长什么样子的
- 使用man 2 bind,可以查看到
- 下面这个结构体其实是第二个参数的具体内容,下面这个东西他其实是一个通用的地址信息
- 上图中的sa_family_t 其实是一个地址域,就是说我们要确定我们在网络层到底使用的是什么样子的协议(到底是4还是6需要确定一下),这个东西占有的字节是2个字节,后面的字符数组占有的是14个字节,也就是说这个结构体一共占有的字节数是16个字节,但是这个其实也不是我们具体的某一个协议所使用的地址信息结构
- 那么他既然不是一个具体的地址信息结构,那么好比说我现在有一个函数,我希望他可以接收任意类型的参数,那么我该如何给出呢?那么我就可以给他给成void*类型的,这个之所以是通用的,就是因为字符数组中并没有指定哪个字节去存储ip地址,哪个字节去存储port端口
- 而我们看到的ipv4版本的ip协议的地址信息结构如下所示:
- 我们可以看出他所占的大小其实就是2个字节
- 另一个部分所占的大小是4个字节
- 我们要将ipv4的信息传递给bind,像下面这样,如果说需要传递的话,其实就需要进行强制类型转换
- 那么,我们为什么不推荐客户端绑定地址信息呢?—其实不是说不绑定,只是说,我们自己不绑定,我们让操作系统去绑定,这里的不绑定不是真正意义上的不绑定,只是说,我们在代码中不去调用bind函数,具体来说一定是绑定了,因为在网络中,我们需要有自己的端口,有了端口之后才知道要把数据交给哪个进程,目的是为了让客户端在同一个进程中开启多个
#include <stdio.h>
#include <unistd.h>
//网络编程
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//这里表示我们创建的是UDP,第一个参数是ipv4的协议,第二个是UDP套接字类型,第三个是UPD的协议
if(sockfd < 0)
{
perror("socket");
return -1;
}
//绑定地址信息
struct sockaddr_in addr; //ipv4的结构体
addr.sin_family = AF_INET; //这一行的意思是进行填充地址域的信息
//1.将点分十进制的ip地址转化成为uint32_t
//2.将uint32_t从主机字节序转换成为网络字节序
// htonl() // 只能完成第二件事情
addr.sin_addr.s_addr = inet_addr("172.16.99.129");//ip; //这一行其实是在填充ip
//如果上面的哪一行不加前面的inet_addr是会出现错误的,因为ip地址其实就是一个字符串,但是填充的参数本质上是无符号32位的整数
//我们是不能直接把一个字符串转换成一个无符号32位整数的,所以需要强制类型转化
addr.sin_port = htons(19999); //端口也要从主机字节序转换成网络字节序
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
//之后就绑定成功了
while(1)
{
sleep(1);
}
return 0;
}
那么上面的代码写出来之后,我们需要去验证两件事情
sockfd是不是文件描述符?
- 我们要去验证sockfd是不是文件描述符,我们只需要去打开问及那描述符表我们就可以验证他到底是不是文件描述符了
- 我们打开文件描述符后,发现他是一个软链接文件,这个软链接文件在闪烁,就表明他的源文件已经不复存在了,但也有可能表明这个文件是对应的内存中的一块缓冲区的,那么这里得socket也再闪其实也表明他其实对应这内存当中得一块缓冲区,所以一直在闪,并不代表socket不存在了
- 从上面来看sockfd确实是文件描述符
是否绑定成功端口了?
- netstat -anp 命令—其功能是可以查看当前操作系统端口占用情况
- 表明当前的19999端口是被UPD所占用的
- 下面表示的是服务器端侦听的ip和端口
- 是能接收的对端(客户端)的ip和端口
- 也就是说,当前19999端口已经被进程号位94870的进程所占用了,也就是说,假如说,我现在要在其启动一次这个进程号的进程其实我就是不能成功的再次启动了,报错信息是当前想绑定的端口已经被其他进程所绑定了,其实也就是告诉我们绑定失败了
- already in use
- 任意表示的是只要他是正常的ip和端口,我都是可以去接收他的
前期准备工作完成之后(创建套接字,绑定地址信息)
- 当前期准备工作完成之后,这个时候,对于UDP而言,他就可以直接给我的服务端去发送数据了,这个时候其实也就是客户端去给服务端发送数据(对于UDP而言,一定是客户端先给服务端发送数据,因为对于UDP的服务端而言,我是不知道客户端的ip的端口的,那么既然我不知道你客户端的ip的端口,我就是无法给你发送数据的,那么这个时候,只有当你给我sendto数据的时候,我接受到了,这个时候我就可以给你去进行回复了,对于服务端来说,需要先接收在发送,如果现在希望结束了的话,那么直接关闭套接字其实就好了
- 一定是客户端给服务端发送数据
涉及到的三个接口
UDP的发送接口
接收接口
关闭套接字
- int close(int fd)
客户端和服务端代码
客户端代码
#include <stdio.h>
#include <unistd.h>
//网络编程
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
int main()
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//服务端的地址信息结构, 包含服务端的ip和port
struct sockaddr_in svr_addr;
svr_addr.sin_family = AF_INET;
svr_addr.sin_port = htons(19999);
svr_addr.sin_addr.s_addr = inet_addr("172.16.99.129");
while(1)
{
//客户端先来发送信息
std::string s;
std::cin >> s;
ssize_t send_size = sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr*)&svr_addr, sizeof(svr_addr));
if(send_size < 0)
{
perror("sendto");
continue;
}
//给成空是因为客户端已经知道了服务端的地址信息结构了,就不需要再接收了
char buf[1024] = { 0 };
ssize_t recv_size = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, NULL, NULL);
if(recv_size < 0)
{
perror("recvfrom");
continue;
}
printf("svr say: %s\n", buf);
}
close(sockfd);
return 0;
}
服务端
#include <stdio.h>
#include <unistd.h>
//网络编程
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
int main()
{
//创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0)
{
perror("socket");
return -1;
}
//绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//1.将点分十进制的ip地址转化成为uint32_t
//2.将uint32_t从主机字节序转换成为网络字节序
// htonl() // 只能完成第二件事情
addr.sin_addr.s_addr = inet_addr("172.16.99.129");//ip;
addr.sin_port = htons(19999);
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
//对于服务端而言,他永远都是在接收数据的
while(1)
{
char buf[1024] = { 0 };
//这个对象并不是我们来进行填充的,因为我们实际上是不知道客户端的信息的,所以我们无法填充
//是recvfrom帮我们来进行填充的
struct sockaddr_in peer_addr; //peer_addr是我们变量的名字,结构体是ipv4的结构体的信息
socklen_t peer_addr_len = sizeof(peer_addr);
//socklen_t peer_addr_len;
//因为是通用信息结构,所以我们需要对他来进行强制类型转换
//最后一个参数是我们需要告诉recvfrom函数我们的地址信息有多长
ssize_t recv_size = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&peer_addr, &peer_addr_len);
if(recv_size < 0)
{
perror("recvfrom");
**//在接受失败这里我们所采取的策略就是让他继续去进行接收,而不是说就停止接收了**
continue;
}
//接收成功的话
printf("cli say: %s\n", buf);
//1.已经拥有了发送方的地址信息结构和地址信息长度
//2.组织要返回给发送方的数据, 并且调用sendto
//给出接收到的信息
std::string s;
//从标准输入中进行读取的操作
std::cin >> s;
ssize_t send_size = sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr*)&peer_addr, peer_addr_len);
if(send_size < 0)
{
perror("sendto");
continue;
}
}
close(sockfd);
return 0;
}
- 运行结果
- 如果不对长度进行初始化的话,可能消息是发送不出去的
- 服务端拿到的端口和我客户端拿到的端口是不一致的,所以这个时候如果想要去发送消息的话,其实是发送不出去的
从上面的代码我们可以看出来
- 客户端:创建套接字,发送数据,接收数据,关闭套接字
- 服务端:创建套接字,绑定地址信息,接收数据,发送数据,关闭套接字
- 我们可以看出来他们其实是有共同的步骤的,那么,我们能不能把这些共同的部分进行一个封装呢?
- udp.hpp—封装一下操作起来更加的方便
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
class UdpSvr
{
private:
int sockfd_;
public:
UdpSvr()
{
sockfd_ = -1;
}
~UdpSvr()
{
//确实是没有什么需要释放的东西
}
//创建套接字
int CreatSocket()
{
sockfd_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd_ < 0)
{
perror("socket");
return -1;
}
return 0;
}
//绑定地址信息
//绑定地址信息涉及到ip和端口,所以我们需要把ip和端口作为参数传入
int Bind(std::string ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(sockfd_, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return -1;
}
return 0;
}
//第一个参数是告诉我们要传递的数据是什么
int Sendto(std::string data, struct sockaddr_in* dest_addr)
{
ssize_t send_size = sendto(sockfd_, data.c_str(), data.size(), 0 ,(struct sockaddr*)dest_addr, sizeof(struct sockaddr_in));
if(send_size < 0)
{
perror("sendto");
return -1;
}
return send_size;
}
int Recvfrom(std::string* data, struct sockaddr_in* peer_addr)
{
char buf[1024] = {0};
socklen_t peer_addr_len = sizeof(struct sockaddr_in);
ssize_t recv_size = recvfrom(sockfd_, buf, sizeof(buf) - 1, 0, (struct sockaddr*)peer_addr, &peer_addr_len);
if(recv_size < 0)
{
perror("recvfrom");
return -1;
}
//data是一个出参,不然你接受到的东西别人是不知道的
//只有这样,调用recv函数的人才知道他接受到了什么东西
data->assign(buf, strlen(buf));
return recv_size;
}
void Close()
{
close(sockfd_);
}
};
- svr.cpp
//首手心啊需要引用一下头文件
#include "../udp.hpp"
//这个宏主要的用意就是来检测我的套接字到底有没有创建成功
#define CHECK_RET(p) if(p < 0){return -1;}
//通过main函数的第三个参数帮我们设置环境变量
//就是说当我这个服务端程序运行起来之后,我需要知道客户端的ip和端口
int main(int argc, char* argv[])
{
// ./svr [ip] [port]
if(argc != 3)
{
printf("using ./svr [ip] [port]\n");
return -1;
}
//创建一个类对象,然后通过对象去调用函数
UdpSvr us;
CHECK_RET(us.CreatSocket());
//argv[1]其实就是ip,2是端口
std::string ip = argv[1];
//以为端口其实是char*类型的,所以我们需要使用atoi把char*类型的数据转换成整型的数据
uint16_t port = atoi(argv[2]);
CHECK_RET(us.Bind(ip, port));
while(1)
{
std::string data;
struct sockaddr_in peer_addr;
int ret = us.Recvfrom(&data, &peer_addr);
if(ret < 0)
{
//如果没接收到的话,就让他重新去进行接收的操作
continue;
}
std::cout << "cli say: " << data << std::endl;
//再send之前,如果data里面有什么东西的话,就把他清空掉就好了
data.clear();
std::cout << "please enter msg to client: ";
fflush(stdout);
std::cin >> data;
ret = us.Sendto(data, &peer_addr);
if(ret < 0)
{
continue;
}
}
us.Close();
return 0;
}
- cli.cpp
#include "../udp.hpp"
#define CHECK_RET(p) if(p < 0){return -1;}
int main(int argc, char* argv[])
{
// ./cli [ip] [port]
// 所要连接的服务端的ip和服务端的端口
if(argc != 3)
{
printf("using ./cli [ip] [port]\n");
return -1;
}
UdpSvr us;
CHECK_RET(us.CreatSocket());
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(port);
dest_addr.sin_addr.s_addr = inet_addr(ip.c_str());
while(1)
{
std::string data;
std::cout << "please enter msg to svr:";
//在缓冲区中,刷新一下缓冲区
fflush(stdout);
std::cin >> data;
int ret = us.Sendto(data, &dest_addr);
if(ret < 0)
{
continue;
}
data.clear();
struct sockaddr_in peer_addr;
ret = us.Recvfrom(&data, &peer_addr);
if(ret < 0)
{
continue;
}
std::cout << "svr say: " << data << std::endl;
}
us.Close();
return 0;
}