前言
我非常佩服那些文章写的好的人,我想了很久这篇文章应该怎么去写,名字怎么起,内容怎么安排,甚至每个内容深入到什么程度。写出来的东西能不能让大家明白?本着我一贯的风格:不光要自己懂,写出来的文章也要让别人看懂;我也是这样来检测自己到底是不是真的懂了。所以我会慢慢的说明,还请大家花点时间慢慢看。
写作原由
2.5年前我有幸成为了一名程序员,从此具备了能改变世界的渺小能力。
当我兴致勃勃的进入到第一家公司后我发现自己什么都改变不了,那时候的我可以说是一个随时准备接受教育的 咸鱼,具体细节在 这里,在第一家公司我迅速的成长了起来,并有幸得到了一份源码,6年经验的上司全程参与,聊天、推送、统计等都是自己做的;我把工程如何启动部分抽离并做成文章 分享了出来。分享出来后确实有很多人从中受益,但是更多的是一些负面评价,那些负面评价的人我2年后再统一回复你们:这个文章对于初学者来说有很明确的指导作用,告诉初学者工程目录结构、
界面编写技巧、权限、职责,编码规范以及一些值得学习的编码经验;
但是对于中高级程序员来说这文章可能一点用处都没有,因为这个阶段的程序员大多都有了自己的
一套编程思想,所以2年之后我再来看这篇文章我也觉得没啥用,
所以负面评价的人这样说也是有原因的。
因为那个项目过于庞大,我也不可能开源出来;那么这篇文章我们就继续写该项目中的另一个部分:Socket自定义协议聊天;当然不是从项目中copy出来,只不过用了该项目中的思路罢了,请你放心这个项目中的Socket部分已经非常成熟,目前在三个项目中运行;也就是说我是有一套完整的聊天源码的,只是我更希望我能自己慢慢的写出来,去理解当中的每一个点。
写作目的
本身的心愿
其实早在一年前,我就开始研究聊天部分了,当时很想自己写一套聊天系统;但是当时的技术实在太菜,不能理解为什么这样写。一年后我再次研究发现自己能慢慢看懂了,在吸收了设计思想后我另起Demo打算试试看,了了我的心愿。提升自己
不知不觉我已经拥有2.5年开发经验了,因为自己的工作环境,我感觉自己和外面的程序世界断了联系,慢慢发现自己快要落伍了;做这个项目的目的也是为了进一步提升自己的技术。给想学习Socket的人一个平台
在我学习Socket时,我在网上找了一些相关的文章,比如:
新手入门IM一篇就够,iOS 即时通讯,从入门到 “放弃”?等优秀的文章,对于一个不懂Socket聊天的人看了之后都有同样的困惑:我怎么开始动手做呢?是的这些文章要不就是太深无法下手,要不就是太简单无法达到真正工作时的要求。那么我现在就做了一个Socket聊天项目,因为是1.0.0版本,所以代码量非常少,只是把大体的样子做了出来;相信你从这个时候参与项目就能真正的动手做了,真正的去实现一套聊天系统。
寻求帮助
自己做一个聊天系统工作量是非常庞大的,文本、语音等消息发送,黑名单等关系处理,单聊、群聊、讨论组等聊天类型;所以我需要借助开源的力量,找到一群愿意一起做的人,我们一起商量、设计完成整个系统。整个系统分为客户端(文中简称C端)和服务器端(文中简称S端)
目前聊天系统主要是两大类
1:CS,也就是有客户端和服务器;
2:P2P,没有服务器客户端和客户端直连。
上面两个各有各的优势,P2P主要做局域网聊天,比如:飞信和飞鸽传书等;
CS才是目前的主流,比如:QQ等。
S端做消息转发和存储,S端没怎么重点设计,我考虑用原本的C来充当S(主要是我并不会Node.js等语言,如果能找到一个人用Node.js或者其他语言实现S当然是最好的):
1:语言只是工具;
2:我们的设计重心在C端;
3:能知道服务器的逻辑能帮我们更好的理解整个聊天过程;
4:C和S都是我们熟悉的OC语言,并不会有任何语言障碍;
5:每个人都拥有完整的系统,自己方便调试与开发;
6:与其他开发人员并不产生依赖,完全独立。
如何参与项目
项目1.0.0我已经托管到github,目前可以这样参与该项目:1:forkC端和S端的源码,切换到对应的分支进行开发:
1:master是可交互的、功能完善的主分支;
2:1.0.0dev、1.0.1dev等都是从master分支克隆的,用来进行对应版本新功能的开发;
这些分支是由我来创建的,你们开发时只需要跟踪当前版本就好了;
3:每个开发版本bug修改、功能开发完毕后才可合并到主分支,这是一个小约定;
4:这里并没有为修改bug单独拉取一个分支,考虑到复杂性,尽量减少外界干扰。
开发完成后New Push Request,我来Agreen;2:进群(289092194加群理由写Sokcet学习交流,我的个人信息里面留的群号是iOS交流群,你也可以加)一起讨论、设计和开发。当然了如果确实没有人愿意和我一起开发,我一个人也会坚持下去的。对于要参与本项目的人,我有以下的几点要求:
1:代码要规范,规范文档我已经放在项目中了;
这样做的目的是为了保持风格统一,让其他人能快速的看懂;
2:注释要全,代码是给人看的;
3:你应具备一定的iOS开发经验;
4:尽量写出可进行单元测试的代码;
5:尽量站在工程师的角度来写代码,性能、速度和内存等可适当考虑;
6:追求质量不追求速度,公司总在赶进度,在这里你可以尽情展示你的代码给别人看;
7:抱着欣赏的角度去看别人的代码。
做聊天系统你应该知道的知识点
TCP/IP协议族
网际互联层还有其他的叫法:网络层等;
网络接口层还有其他的叫法:数据链路层、链路层等;
大家知道分别指哪一层就好了。
我们提到TCP/IP一般就指TCP/IP四层模型,TCP只是传输层的一个协议,而IP也只是网际互联层的一个协议。TCP/IP是一套定义在硬件层面数据传输的规则,定义这样一套规则就是为了让不同类型数据经过源地址到目标地址中各个传输介质时能差别处理并传输罢了。
传输协议选TCP还是UDP
这是一个必然会遇到的问题,我们在这里也不用讨论了,多年行业实践经验已得出结论:
1:团队很牛逼,用UDP或者UDP+TCP;
2:团队一般,用TCP。
所以我们用TCP作为我们聊天系统的传输协议。
TCP、IP协议
从这张图中我们可以看出你用TCP协议,那么网络层肯定是得用IP协议了,网络接口层用什么协议需要看情况,比如有网线、无网线等;TCP是可靠消息传输协议,头部定义这么多标志位是为了能够把数据 尽量送到目的地罢了。也就是说如果我们用TCP协议发送"hello"这个用户数据,那么到了TCP层会加上TCP头部组成TCP段,到了IP层会加上IP头部组成IP数据报,到了网络接口层会加上头尾部组成帧,这时候才开始在传输介质中传输,到达目的地后每层协议再剥掉相应的首尾部,最后得出用户数据"hello";更复杂的情况可以看 这里。
TCP连接时的三次握手、四次挥手
这部分请大家看这篇好文,看完之后我来一下总结:
三次握手:当我们自己组装(请注意体会这个词)的TCP段满足第一次握手格式,发送到目的地后
,目的地如果支持TCP连接,那么它就会回复你第二次握手的信息,
你收到第二次握手信息再组装第三次握手格式的TCP段发送过去就完成了连接操作;
四次挥手:道理类似。
为什么需要三次握手和四次挥手请看这里;我们可以像这个哥们文章一样自己封装TCP段模拟三次握手;当然了你也可以了解一下Charles抓取Https原理。
Socket
上面说了我们要用TCP协议传递数据,就要懂报、段、帧、握手和挥手,甚至IP分片、超时重传和滑动窗口等,让我们自己来实现就太麻烦了。Socket已经帮我们做好了这一切,我们只需要用Socket提供的接口就可以,具体哪些接口可以看这里。看了这些你可能还是觉得很麻烦。所以才有了CocoaAsyncSocket这个三方库,它对用户隐藏了麻烦的的Socket操作,提供给用户面向对象的OC接口;所以本系统也是选用CocoaAsyncSocket进行二次封装。
Socket自定义协议到底是什么
我们用CocoaAsyncSocket中的GCDAsyncSocket类就可以使用TCP传输数据、GCDAsyncUdpSocket类就可以使用UDP传输数据了;也就是传输控制层及其以下的协议我们都不可能去自定义了,那么我们自定义协议自然就是定义的用户层协议。
实现聊天目前已存在的常见用户层协议
如果你并不想自定义用户层协议,那么你可以从这里进行已有用户层协议选择,并直接使用提供的SDK进行开发。聊天系统当然要分C和S,前面也说了我们的S也是用C来充当的(再次说明一下)。
本系统协议内容部分讲解
这里并不会讲太多,因为源码你可以直接下载下来,现在是1.0.0版本代码量很少,你可以通过写好的连接、登录、心跳流程来理解整个项目内容;如果你说看不懂那么你可能还没有到学习Socket的地步,可以过段时间再来理解。
说这句话并没有任何恶意,因为任何事情都是一步一步来的,
在什么阶段学习什么内容,强行吸收不能掌握的内容只会适得其反。
包格式定义
发送数据时比如我们发送一条数据"hello world",可能目的地前后收到了两条数据"hello "和"world",因此我们需要进行分包处理;分包部分原因:
1:以太网限制在46-1500字节,1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量
进行分片传输,现在一般可允许应用层设置8k(NTFS系)的缓冲区,
8k的数据由底层分片,而应用看来只是一次发送;
2:路由器等也是可以设置大小的。
发送数据时比如我们前后发送两条数据"hello "和"world",可能目的地只收到了一条数据"hello world",因此我们需要进行粘包处理;粘包部分原因:
1:TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送
一段数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,
这样接收方就收到了粘包数据。
因为TCP/IP是尽可能提供可靠传输,传输过程还是可能会丢包或者接收到错误的包,所以我们还需要进行错包处理。针对这三种问题我们都可以为将发送数据加一个头部进行解决,可在项目IMSocketHeader中看到;目前只在包头用4字节放了4个标志位,每个标志位占1字节:
1:version放用户当前聊天系统版本,用于后面聊天发布新功能时,旧聊天版本不启用部分功能等;
2:magic_num用于确定包是不是一个我们应该处理的包,目前写死;
3:command存放命令类型区分心跳、登录等,因为包不同客户端、服务器处理方式有很大不同;
4:body_len内容长度,用来处理粘包和分包以可到一个完整的包,
具体处理流程在IMSocketIO中看到。
为啥要进进行粘包、分包和错误包处理:
只是为了得到一个完整的解析单元而已,也就是得到一个完整的我们定义的包(头部+内容)。
数据格式
通过加包头我们丢给TCP层的数据就变成了:包头+内容;那么我们把内容以什么样的格式发送出去呢?
这里我们把要发送的对象转成JSON字符串再转成NSData进行发送。
并没有选用Protobuf的原因如下:
1:并不是所有想参与项目的人都能顺利安装、使用Protobuf环境;
2:数据格式在整个系统设计中并不重要,它更属于后期优化内容;
3:对于JSON我们更为熟悉,使用起来更快捷方便。
部分项目中重要的类
其实目前项目中的连接、登录等流程已经把工程中所有的类都使用完了,你可以顺藤摸瓜的去走一遍流程就知道各个类的作用和意义了。不过还是有几个核心类需要单独拿出来提一下。
IMSocketModules
需要接收的消息在本类中注册,收到未注册的消息内部会自动丢弃;这有点像MQTT了。
IMSocketControl
超时重传、心跳、数据加解密、收到未注册的消息内部会在本类丢弃。
服务器项目简单介绍
1:采用基础工程快速搭建;
2:项目中主要就是一个类ChatSocketServer用于接收客户端的连接、登录、退出、心跳等请求,之所以用一个文件因为本系统重心是在客户端;
3:因为聊天是独立于应用模块的,所以服务器不做登录用户验证,也就是任何登录信息合法的客户端进行登录服务器都会与之建立连接;
4:数据库用Realm来缓存消息;
5:版本迭代内容在项目中Readme.md中实时更新。
客户端项目简单介绍
1:采用基础工程快速搭建;
2:目前应用中模拟了3个死用户,后期会考虑注册功能,但是目前用不到;
3:有一个简单的UITabBarController作为主控制器,里面有三个界面:会话、好友、我;目前只在我界面展示了用户信息,其他的界面后面慢慢迭代;
4:编写界面时采用MVC逻辑与视图完全分离,可以看看ChatMineController,这也是参照Andoird来写界面,因为本身我也是一个Android开发者,最近打算试试这样开发界面的可行性,当然了你参与项目时你可以在你负责的模块中使用MVP、MVVM等;
5:数据库用Realm,这样做可以实现数据库驱动界面实时更新,主要是用了Realm的addNotificationBlock方法;
6:在SocketIMDemoTests中创建和主工程对应文件夹对应文件进行单元测试;
7:项目使用组件化;
8:版本迭代内容在项目中Readme.md中实时更新。
文末
我从准备项目到写完这篇文章不知不觉过去了半个月,这一切都是值得的,至少目前我更了解TCP/IP协议了。欢迎大家加群和fork项目,不管你是大牛还是想学习的小牛,我们都欢迎你的到来,千万不要低估了自己的力量。最坏的情况就是到最后(初步定于2018年3月底开始做1.0.1dev版本)还是只有我一个人,那么我还是会坚持写下去的,只是迭代周期会长很多。
问题反馈区
在项目发起后一周,已经陆续有20人参与进来,参与过程中问题也是很多的,所以专门在这里记录下来
怎么运行项目?
因为项目采用的是组件化,设置的工程依赖,所以你每次拉取代码后最好都做一下如下的操作:
1:Clean工程;
2:依次编译Common、IMServer、ModelManager、HttpServer和SocketIMDemo。
3:Run工程。
运行时Realm报错
你可能会遇到下面的错误原因:
所有存入Realm数据库的模型,如果有改动都需要进行升级,
考虑到开发过程中模型一直会不停的变,所以我并没有写升级代码。
解决方案:
卸载重装应用就好了。