网络基础二
文章目录
- 协议定制,序列化和反序列化
- 应用层
- 网络版计算器
- 协议的定制
- 序列反序列化
- 序列化
- 未复用版
- 反序列化
TCP是面向字节流的,你怎么保证,你读取上来的数据,是‘’一个“ “完整””的报文呢?
我们没有区分字符串里面有什么内容,连字符串都没有区分,我给你发了个你好,服务器这里read就一定能读到个你好 吗?
对方发过来因为网络问题只发过来一个你而已,但是之前并没有这个问题啊,那是因为网络好,数据量也不大。
就像之前管道写了好几次,但是读端把数据一下子全读上来了。
为什么觉得文件操作恶心,文件部分也和协议有关。
比如我把数据写到文件里,可能连续写了十几条消息,写好写,可是读的时候,里面有字符串有整数,当我读取时比较困难的把数据反向转换回来,且读的时候可能人家写了十几次我要读我调用一个fread把整个文件全都读进来了,此时你怎么保证你读上来的就是一个完整的拿到一段你想要的数据呢?
这个其实比较困难,因为这里有知识的残缺,必须解决。
那我按行读取不就完了吗,按行读取仅仅是对文件一种读取方式,那我一大堆二进制数据,你怎么保证读上来了关键字段呢?
正确对文件读写,对管道读写,对网络读写,尤其是面向字节流,你怎么保证你读上来的是一个完整的报文呢?
则今天服务器的read是有BUG的。
我们今天拷贝字符串从C到S,可是服务器就是从接受缓冲区里读,造成接受缓冲区快被打满了,此时客户端发数据可能只发了一部分这种情况
作为用户只需要在应用层通过write把数据拷贝到TCP发送缓冲区里,write调用直接返回,这个函数调用完了,可是这个数据不一定已经发给对方接受缓冲区!
数据不一定发送到网络里,所以write ,read,send , recv这样的接口不叫网络收发,实际上他的作用就是用户到内核拷贝,而真正决定网络收发的协议是由TCP协议决定的,因为只有TCP协议比较了解网络当前的健康状态和接收方的接受能力,所以必须由TCP全权负责:
1.什么时候发?
2.发多少?
3.出错了怎么办?
TCP协议是不是在OS内部实现的?
属于OS编译好的一部分。
所以OS属于内核,你所谓的用户把数据交给TCP这句话有点不太可信,实际上是用户把自己的数据交给了OS了,那你放心OS吗,当年讲文件操作时,其实是把用户的数据拷贝到文件的缓冲区里,最后由OS来把文件缓冲区的数据定期刷新到磁盘,如果你不相信OS,为什么当年你要相信呢?
所以用户把数据交给TCP,是用户把自己的数据交给了OS!
所以文件操作叫log.tx,今天的文件就叫做TCP。
此时把数据从用户层拷贝到内核你就别管了,作为用户直接返回就行了,OS会决定定期发多少。
发送谈完了再来谈谈接受,这时候就有点搞笑了,怎么搞笑呢?
作为接收方来讲,他要通过read调用,说白了是把接受缓冲区的数据拷贝到用户层缓冲区buffer[]
所以对方给我发了多少数据,什么时候发的,有没有把完整的数据一次全打包还是分三份打包过来?
接受缓冲区接受到的数据完全由OS决定了,对方发的时候可能把整个字符串分了三份发过来,也可能整体打包发过来,甚至把发了三四次写的数据最后打包整体发过来了。
假设读的时候一次全读完,有时候还读不完。
假设把缓冲区一次全读完,到底把数据有没有读到在应用层是一个我所认识的完整的报文,是一个字符串的一部分,还是读上来三四个报文,我们这里就完全不确定了,所以要对读上来数据进行更细节的分析。
所以在应用层我们要清楚,我们就必须得保证在应用层把协议定好,我们才能更好的进行读上来的数据的分析。
协议定制,序列化和反序列化
我规定好通信双方使用固定大小的报文,比如发送端是64字节,我在read返回值小于64字节,这个报文就不完整,我就把报文继续维持起来,当我下次继续读的时候一定要凑够64字节然后进行处理。
相当于把文件固定大小每次写一行,读也读一行,也叫订协议。
应用层
网络版计算器
方案一
发送“1+1”字符串,然后以类似方式返回
这样不太好,如果一下发了三四个请求,该如何区分呢?
方案二
定义请求结构体,直接把结构体大小字节的二进制数据发给对方,对方使用同样类型的结构体就可以提取出整数 a和整数 b,结果也是结构体处理
通信双发不发字符串,直接发对象,可以吗?
这是肯定可以的。但是在应用层不建议。
问题
- 同一个结构体先后在不同的编译器下编译出来的同一个结构体的大小一定是一样吗?
在内核层这样搞没问题,因为不管Windows还是Linux你们用的协议都叫TCP/IP协议。
可是用户应用层的编译器就千差万别了,编译出来的大小可能就是不一样的,因为有结构体对齐,他按8,他按4,大小就不一样。
比如说网络带宽特别差,接收方接受缓冲区已经被打满了,接收方给发送方说我来不及接收了先别发,所以TCP发送方就不发了,可是用户不知道,他就一直拷到TCP发送缓冲区里,四五个报文全在TCP缓冲区里,后来接收方说我好了你发吧, 我可以接受很大数据,TCP把四五个请求全给他发过去了,四五个请求是纯二进制的,那你怎么去区分一个一个报文呢?
我们该如何设计网络版本计算器的协议?
协议 = 结构体
整个结构体当中每一个结构体每一个字段都要让客户端和服务端约定好的。
约定好之后使用结构化的方式把约定表达出来,这就叫做定义出来了协议。
协议的定制
我们在微信聊天的时候约定通用结构体,这就叫定了聊天协议。
序列反序列化
我们在网络通信的时候,整个结构化的数据把多个字符串转化成一个字符串,这个过程称之为序列化。
把一个字符串打散成多个字符串转换成结构体化数据,叫做反序列化
我们最终为什么要序列反序列化呢?主要是序列反序列化之后方便网络进行收和发。
那发过去的一个大字符串,收过来一个字符串,我怎么保证把字符串收全呢?
后面还要设计报文和报文之间的分隔符。
所以方案一和方案二我们两个都用了。
不把多字符串整个成一个,那一个一个发,昵称啊,内容啊,时间啊,对端还要把多个字符串关联起来,难度太大。
我们要把创建套接字,绑定,监听,accept,connect全部封装成一个类Sock,不然写到Tcpserver类里面就很混乱。
这样Tcpserver调用起来逻辑很清晰。
客户端同理
让客户端和服务器双方使用同一套接口就行,不用来回写车轱辘代码了。
从此往后我们再也不写套接字了,因为我们有了小组件Socket.hpp的类Sock
前面说了要定制协议,就是设计Request和Response结构体,再把结构化的数据序列化成一个大字符串
我们的请求计算器可以从
int x int y char op
变为 字符串 “x op y”
接受缓冲区收到这种风格的字符串,也能解析,可是对方发来了多个这样的字符串,上层通过调用read();读取字符串的时候,他怎么保证上层读上来的是一个完整的报文呢?
有没有他读上来的是半个,一个半,两个。
如果你读上来的是两个,你怎么把这两个区分开?
如果你读上来的是一个半,你怎么证明一个半当中,哪一个是当前的,哪一个是下一个。
如果读上来是半个呢,你怎么知道报文多长呢?
请求和请求之间用特殊符号把他们分隔开
当上层读的时候碰到\n字符,\n之前的所有字符全部都收到了,就可做到报文和报文之间隔开了。
假设把缓冲区全部读上来,之后的问题就是字符串分析
把整个报文用\n分开,左边一个报文,右边一个报文。
不一定非得用\n,你可以用\3或者绝不会在报文中出现的分隔符
计算器场景比较简单,用\n分割一个个报文,这样我们再做字符串分析绝对可以分出一个一个子串,然后再对子串切割绝对能恢复成结构体
这样做有点简单,另一方面还是不够完善,因为有可能读上来的字符根本不是你想的那样,如果缓冲区只有半个报文呢,你怎么保证你读上来的是一个完整的报文呢?
有人说我只要判断有没有\n就可以了,没毛病,这样肯定可以的,不过今天我还想加一个东西,我想在报文前再加一个字段,长度
这个长度字段可以固定大小,也可以是字符串
这个长度是整个完整报文的长度是多少,这个长度不包括\n
我们还可以添加一个协议类型报头,让计算器支持浮点数或者整形计算。
今天就不这么复杂带这个协议类型了
最终的报文字符串风格就是这样子
将来读取的这一方,他可以一直读到\n,就可以把第一个字段读上来,也就是读到了第一个报文的长度。
也可以把全部缓冲区的报文都读上来,读上来之后根据第一个\n分析清楚是9,就知道第一个报文的长度,从第一个\n往后连续读取9个字符,我就能保证读到了一个完整的报文了。
100 + 200 \n这个\n一会再说。
后续字符串同理处理。
有人说200 后面的\n 要不要其实都无所谓了,因为已经有长度了,为什么还要带呢?今天把他带上未来想调试的话printf cout 直接打出来就会变成如下的样子。
很直观就能看到协议定制效果。
序列化
未复用版
请求“len\nx op y" y后面的\n其他地方加
反序列化
我们除了要解决一个报文内部的问题,这是序列化和反序列化
我们还要解决报文和报文之间的问题
课上代码因为在序列化过程中无论是客户端还是服务端都需要封装报头长度,外加最后的\n,所以不如在设置Encode来复用,他们两个都可以加报头和尾部\n
Encode
对"x op y" 添加报头长度\n 和 尾部\n
Decode函数
从len"\n"x op y"\n提取有效报文 “x op y”
内部
1.查找第一个\n,如果没找到直接返回,说明长度报头没有,就不玩了。
2.整个报文的长度应该是len(x op y的长度)+len长度报头本身的长度+2 (两个\n)
对要Decode的len"\n"x op y"\n…进行长度检查,如果整个读上来的报文长度不够说明这不是一个完整的报文请求,我也不玩了。
3.利用substr切出有效报文的子串
4.从package大长字符串移除此报文
一 把网络功能写出来
二 协议定好
三 把这俩货捏在一起 servercal.hpp Tcpserver
在tcpserver类中设置回调函数, 涉及了bind,bind的是calculato函数