目录
初识网络
网络的概念
局域网通信
如何理解报头
IP和mac
udp——代码实现
tcp——代码实现
先有计算机,然后有OS,最后才有了网络,而网络则是为了提高效率的!
局域网LAN: 计算机数量更多了,通过交换机和路由器连接在一起!
广域网:只是一个相对的概念,可以看成是一个范围更广的局域网!
网络的位置
软件是可以分层的!而网络也是软件,所以网络协议栈也是层状结构,就再正常不过了!
这里的网络,指的是网络协议栈,它是一个软件,贯穿体系结构的tcp/ip,属于OS的一部分
认识协议
例:在2000年的时候,小明在外地上大学,而因为电话费很贵,所以他和他爸做了个约定,来节
省电话费,小明给家里打电话,电话响一声,小明就挂掉,表示小明是平安的;响两声,小明就挂
掉,表示小明没钱了;响三声,表示小明有其它事情要处理,需要他爸接电话沟通,而这样的约
定,就是对协议的感性认识!
网络协议的概念
所谓的计算机协议,本质就是约定,约定是由编码的程序员自己根据标准文档(只约定好协议,是
不够的,因为计算机操作系统,生产厂商等都有很多,所以就需要有一个人,来约定共同的标
准,大家遵守),或者自己的喜好定义协议!
OSI七层模型
OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模
型,是一个逻辑上的定义和规范
TCP/IP五层(或四层)模型
物理层: 负责光/电信号的传递方式,物理层的能力决定了最大传输速率、传输距离、抗干扰性等,
集线器(Hub)工作在物理层
数据链路层: 负责设备之间的数据帧的传送和识别,交换机(Switch)工作在数据链路层
网络层: 负责地址管理和路由选择,路由器(Router)工作在网路层
传输层: 负责两台主机之间的数据传输
应用层: 负责应用程序间沟通
例:如下图,是一个洗发水快递的输送过程,卖家首先需要将洗发水找快递公司去封装,比如贴快
递单, 包装洗发水等等,然后发货,而从卖家运输到客户,走的是一条什么样的路径(路径规
划)是由网络层决定的,而具体从这一站到下一站,则是由数据链路层决定,比如从浙江到云南,
而如果客户在收到快递后,发现产品破损,联系卖家,卖家重新发一份,传输层所做的就是卖家此
时的工作!
注意:数据传输给对面,事情还没完,这只是第一步,还有第二步,如何分析和使用数据,这是由
应用层去做的
局域网通信
如下图,两台主机通信,是需要走完所有层,经过以太网传输数据给对方,而不能在某一层直接跨
越过去,比如:你朋友在宿舍另外一栋,你在这一栋,那你要将他的文件给他,就必须先下楼到一
楼,然后再上楼给他!
如下图,数据在发送的传输过程中,在每一层都会添加报头,而在收到的传输过程中,在每一层都
会去掉报头!而在用户看来,两个人之间是直接通信的!
如何理解报头
从生活角度理解
所有的快递盒子上,都必定有快递单,而其中最重要的就是它的格式+数据,它就可以称为快递的
报头,因为没有它,就无法得知,该快递如何派发,所以需要报头中的数据,来指导当前层进行某
种协议决策!
在计算机OS中理解报头和数据
每一层添加的就是报头,其它的则是数据,比如上图,在网络层添加的五边形就是报头,另外的
则是数据,该数据也被称为有效载荷!
报头也是数据,而且是一种结构化的数据,又因为Linux是C语言写的,站在语言角度去理解,就
有了下图中的报头!添加时只需要用memcpy或memove拷贝到缓冲区即可!
如下图,在传输数据给对方解包的过程中,几乎每一层协议的报头中都要包含两种字段,这两种字
段也是协议的共性!第一种用于分用,第二种用于解包
1,当前报文的有效载荷要交付给上层的哪一个协议!比如是交给TCP,还是UDP
2,几乎每个报头,明确报头和有效载荷的边界,防止多解或者少解!
如下图,是一个局域网,B向G发送消息的时候,报文中包含G的mac地址,而光电信号是无法将
其直接发送给G的,所以每一台主机都会收到这个报文,而其它主机在收到时,因为不是它的,就
会将其丢弃,只有G才会接收!比如,在教室上课,老师喊张三回答问题,就只有张三会站起来,
其它人会将老师的话忽略掉
因为网络资源被大家所共享,所以如果两台及其以上主机同时发送,就可能发送数据碰撞的问题,
所以每台主机都要有碰撞检测的能力,以及碰撞避免算法!!!
因为网络资源为大家所共享,所以它也是临界资源!局域网中任何一个时刻,都只能有一台主机在
向局域网中发送消息,说明各个主机访问网络资源是"互斥"的!
如果想攻击这个局域网,就可以一直占用网络资源,使其它主机出现"饥饿"问题!
IP和MAC
报文是有两套地址的,一套是IP地址,另一套则是MAC地址,IP地址是从哪里来,到哪里去,几
乎一直不变,比如你要从湖南经过多个省份到达北京,起始地址和终止地址始终没变,而MAC地
址是上一站从哪里来,下一站去哪里,一直在变化,起始地址和终止地址一直在变,因为它的上一
站和下一站在变!
如下图,主机A发送消息给主机B,报文中的报头在经过路由器后发生了巨大改变,将原来的报头
解包后,重新封装了令牌环的报头,但它的有效载荷是一定不会变的!要不然这通信就没意义了!
而有了路由器屏蔽下面的操作后,IP上面的也就看不到底层网络的任何差异!
报文在数据链路层,经过路由器,因为一直在进行解包和封装!所以MAC地址一直在变化
源IP,目的IP:对一个报文来讲,从哪里来到哪里去,而最大的意义则是指导一个报文该如何进行
路径选择,到哪里去:本质就是让我们根据目标,进行路径选择的依据!
数据从A主机到达B主机不是目的!而是要让数据到目标主机B上的一个进程,让该进程提供数
据处理服务
计算机本身不产生数据,产生数据的是人!人是通过特定的客户端,产生的数据!比如你下载一个
app,然后注册,就会起昵称等等
端口号
IP地址(公网IP):唯一标识互联网中的一台主机
本质上,所有的网络通信,站在普通人的视角,都是人和人之间通信
技术人员的视角,我们学到的网络通信,本质是:是进程间通信!比如:抖音的app客户端(进程)
<->抖音的服务器(进程)
如下图,IP仅仅是解决了两台物理机器之间互相通信,比如主机A与B,但是我们还要考虑如何保
证双方的用户之间能看到发送的和接收的数据,比如抖音app客户端与抖音服务器,要让这两者都
看到发送和接收的数据,不能发送到其它进程,所以就有了端口号!!!
端口号:唯一的标识一台机器上的一个进程!
sockct
IP + PORT = 能够标识互联网中的唯一的一个进程!!!
整个网络可以看作是一个大的OS,所有的网络上网行为,基本都是在这一个大的OS内,进行进程
间通信!!!
ip地址 + port端口号 = socket(套接字)
进程的PID与PORT的区别
例:PID就相当于我们的身份证,PORT就相当于我们的学号,虽然身份证号可以唯一确定一个
人,也就可以直接用身份证号来充当学号,但这样做不好,其一,学号里面的数字有许多含义,例
如确定了你是哪个学院的哪个班级等等,其二,不会受身份证号的影响,假设某天国家不使用身份
证号了,如果用身份证作学号,也就可能无法唯一标识学校的某个人了
要进行通信,本质:先找到目标主机;再找到该主机上的服务(进程),总结:互联网世界,是一
个进程间通信的世界!
因为进程具有独立性,进程间通信的前提工作,先得让不同的进程,看到同一份资源,而这里的这
个资源则是网络!
一个进程可以关联多个端口号,但一个端口号不能关联多个进程(因为不符合端口号的定义)
TCP是可靠的,而UDP是不可靠的!注意:这里的可靠是中性词!!!
网络字节序
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
因为主机有大端机和小端机之分,所以都按照TCP/IP规定的网络字节序来发送/接收数据,如果发
送主机是小端,就需要先将数据转成大端
网络通信的标准方式有很多种,基于ip的网络通信 AF_INT,原始套接字,域间套接字
udp——代码实现
服务器端
首先需要创建套接字
如下图,是创建套接字的接口,参数domain,是套接字的种类,这里传AF_INET,参数type,是
套接字的类型,这里是udp,所以传SOCK_DGRAM,最后一个是协议种类,这里传0即可!返回
值,这个接口的创建与文件类似,失败就返回-1
给该服务器绑定端口和ip
服务器的socket信息(ip+port),必须得被客户知道!一般服务器的port,必须是众所周知的
(不仅是被人,也可以是各种软件,app,浏览器等)的,而且轻易不能被改变!
首先设置一个端口,并创建一个结构体,同时需要将计算机中的变量(主机序列)转变为网络字节序
另外,需要解决两个问题,而inet_addr函数可以解决
1.需要将人识别的点分十进制,字符串风格IP地址([0-255].[0-255],[0-255].[0-255]),转化为4字节
整数IP
2.需要考虑大小端
因为云服务器,不允许用户直接bind公网IP,另外,实际正常编写的时候,我们也不会指明IP,如
果用上面那种方式,意味着只有发到该IP主机上面的数据才会交给你的网络进程,但是,一般服务
器可能有多张网卡,配置多个IP,我们需要的不是某个IP上面的数据,而是发送到该主机,该端口
的数据!!!所以采用下图这种方式更好!
然后对绑定失败的情况做一下处理,同时要对参数中的结构体做一下强转
如下图,是绑定端口和ip的接口,参数sockfd是创建套接字的返回值,addr是需要用户指定服务
器的相关socket信息,addrlen是传入结构体的大小,成功返回0,失败返回-1
提供服务
如下图,是读取客户端发送的信息
如下图,是读取信息的接口,参数sockfd是创建套接字的返回值,参数buf是缓冲区,参数len是
其长度,参数flags是读取方式,设为0即可!下面两个参数,则是和服务端通信的客户端的信息!
返回值返回的是读取了多少个字节,如果读取错误则返回-1
给客户端回复信息
如下图,前面几个参数和返回值与上面的读取信息类似,最后两个参数则是表明给谁发送信息!
客户端
创建套接字
客户端不需要显示的bind,首先,客户端必须也要有ip和port,但是,客户端不需要显示的bind!
一旦显示bind,就必须明确,client要和哪一个port关联,但client指明的端口号,在client端不一
定会有,可能被占用,被占用导致client无法使用,server要的是port必须明确,而且不变,但
client只要有就行!一般由OS自动给bind(),在client正常发送数据的时候,采用的是随机端口的
方式!
使用服务
数据从哪儿来
你要给谁发
这里借助了命令行参数
读取服务器端回复的信息!
运行结果
如下图,是本地测试的结果,这里用上面的IP地址可能测试不了,所以可以用127.0.0.1来测试,当
然,如果想要两台主机之间通信,也可以去试试
代码调整
我们默认认为通信的数据是双方在互发字符串,所以还需要对上面的代码做一些调整
对服务器端
对客户端
注意:在网络通信中,只有报文大小,或者是字节流中字节的个数,没有C/C++字符串这样的概念
如下图,我们不把服务器端的端口给写死了,采取命令行参数的形式来输入端口,更加灵活!
应用场景
给服务器端发送命令,服务器端将执行后的结果发送给客户端
对服务器端
如下图,这个文件是一个管道文件,可以执行命令,底层原理是通过创建子进程来执行命令,执行
后的结果会放在文件中等你去读取!
对客户端
运行结果
TCP——代码实现
对服务器端
前面的代码和udp就只有创建的套接字的类型不一样,其余都一样
建立连接
tcp是面向连接的,udp是无连接的,所以tcp需要在通信前,需要建连接,才能通信
设置套接字是listen状态,本质是允许用户连接
如下图,参数sockfd是创建套接字的返回值,参数backlog暂时设置为5即可!
客户端主动建立连接,因为它需要服务,服务器端被动接受连接,因为它得提供服务
我们当前写的server,是一个周而复始的不间断的等待客户到来,要不断的给用户提供一个建立连
接的功能!
如下图,创建套接字和接收连接的接口的返回值都是文件描述符,但两者有很大区别,比如在一个
旅游区的其中一条商业街上,有许多饭店,张三就是其中一家饭店负责揽客的工作人员,每当他揽
到一次客,他就会喊服务员李四、王五等来给客人提供服务,自己则继续去揽客,所以张三就是监
听套接字,而李四、王五则是提供IO服务的fd!
如果连接失败,继续去连接,就如同张三招揽的客人不来吃,那他也无法勉强,只能继续去招揽客
人!所以直接continue
提供服务
如下图,因为tcp是面向字节流的,就如同文件一般,可以进行正常的读写
对客户端
前面的和udp只在创建的套接字的类型不一样,其余的都一样!
为发起连接准备参数
如下图,类似于memset,将结构体变量的所有字节置为0!
发起连接
如下图,是发起连接的接口,成功返回0
业务请求
运行结果
上面的是单进程版本的,没什么用,因为只能建立一个连接,其它进程想连接时会被阻挡在死循环
外面,所以需要做一个多进程版本的!
在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源,父进程无需等待
如下图,在死循环里面创建子进程,同时将提供服务的代码都放在函数ServiceIO中
如下图,如果想知道是谁连接到服务器,也可以打印它的ip和port
将端口号从网络数据转化为主机数据!
将ip地址从一个四字节整形数据转化为字符串数据!
version 2
如果不关闭不需要的文件描述符,会造成文件描述符泄漏!而因为子进程会继承父进程打开的fd,
所以无论父子进程中的哪一个,强烈建议关闭不需要的fd!对于listen_sock,子进程也需要在服
务器提供服务前将其关闭,否则子进程可能会对其进行误读!
version 2.1
父进程需要等待,因为子进程创建进程后退出,需要有人来清理它的资源,但不再是阻塞等待,而
是非阻塞等待,子进程创建出的进程,也就是孙子进程,成为了孤儿进程,孙子进程去提供服务,
退出后由操作系统去清理它的资源!
version 3
多线程版本,主线程打开的fd,新线程能看到,因为是共享的,所以listen_sock不能关!
缺点:创建线程,进程无上限;当客户链接来了,我们才给客户创建线程,就如同饭店来客人了,
你才去招厨师,服务员,效率太低了!
version 4
为了解决version 3的两个缺点,可以采用线程池的方式,同时将服务代码放在task任务类的一个
成员函数中
总结
创建socket的过程,socket(),本质是打开文件——仅仅有系统相关的内容
bind(),struct,sockaddr_in -> ip,port,本质是ip+port和文件信息进行关联
listen(),本质是设置该socket文件的状态,允许别人来连接我
accept(),获取新链接到应用层,是以fd为代表的——链接:当有很多个链接连上我们的服务器
时,OS会存在大量的连接,就需要去管理,管理方式还是先描述,再组织。所谓的"连接",在OS
层面,本质上就是一个描述连接的结构体
read/write,本质就是进行网络通信,但对用户来说,相当于我们在进行正常的文件读写
close(fd),关闭文件,系统层面,释放曾经申请的文件资源、连接资源等;网络层面,通知对
方,我的连接已经关闭了!
connect(),本质是发起链接,在系统层面,就是构建一个请求报文发送过去;在网络层面,发起
tcp连接的三次握手!
close(),client和server,本质在网络层面就是在进行四次挥手!
如下图,我们前面写的代码都用到了一些系统调用接口,而这样做的目的是在从零开始编写应用
层,不是在使用应用层!