前言
我非常佩服那些文章寫的好的人赦政,我想了很久這篇文章應(yīng)該怎么去寫胜宇,名字怎么起,內(nèi)容怎么安排恢着,甚至每個(gè)內(nèi)容深入到什么程度桐愉。寫出來的東西能不能讓大家明白?本著我一貫的風(fēng)格:不光要自己懂掰派,寫出來的文章也要讓別人看懂从诲;我也是這樣來檢測(cè)自己到底是不是真的懂了。所以我會(huì)慢慢的說明靡羡,還請(qǐng)大家花點(diǎn)時(shí)間慢慢看系洛。
寫作原由
2.5年前我有幸成為了一名程序員,從此具備了能改變世界的渺小能力略步。
這個(gè)文章對(duì)于初學(xué)者來說有很明確的指導(dǎo)作用屎飘,告訴初學(xué)者工程目錄結(jié)構(gòu)妥曲、
界面編寫技巧、權(quán)限钦购、職責(zé)檐盟,編碼規(guī)范以及一些值得學(xué)習(xí)的編碼經(jīng)驗(yàn);
但是對(duì)于中高級(jí)程序員來說這文章可能一點(diǎn)用處都沒有押桃,因?yàn)檫@個(gè)階段的程序員大多都有了自己的
一套編程思想葵萎,所以2年之后我再來看這篇文章我也覺得沒啥用,
所以負(fù)面評(píng)價(jià)的人這樣說也是有原因的唱凯。
因?yàn)槟莻€(gè)項(xiàng)目過于龐大羡忘,我也不可能開源出來;那么這篇文章我們就繼續(xù)寫該項(xiàng)目中的另一個(gè)部分:Socket自定義協(xié)議聊天磕昼;當(dāng)然不是從項(xiàng)目中copy出來卷雕,只不過用了該項(xiàng)目中的思路罷了,請(qǐng)你放心這個(gè)項(xiàng)目中的Socket部分已經(jīng)非常成熟票从,目前在三個(gè)項(xiàng)目中運(yùn)行漫雕;也就是說我是有一套完整的聊天源碼的,只是我更希望我能自己慢慢的寫出來峰鄙,去理解當(dāng)中的每一個(gè)點(diǎn)浸间。寫作目的
本身的心愿
其實(shí)早在一年前,我就開始研究聊天部分了先馆,當(dāng)時(shí)很想自己寫一套聊天系統(tǒng)发框;但是當(dāng)時(shí)的技術(shù)實(shí)在太菜,不能理解為什么這樣寫煤墙。一年后我再次研究發(fā)現(xiàn)自己能慢慢看懂了梅惯,在吸收了設(shè)計(jì)思想后我另起Demo打算試試看,了了我的心愿仿野。提升自己
不知不覺我已經(jīng)擁有2.5年開發(fā)經(jīng)驗(yàn)了铣减,因?yàn)樽约旱墓ぷ鳝h(huán)境,我感覺自己和外面的程序世界斷了聯(lián)系脚作,慢慢發(fā)現(xiàn)自己快要落伍了葫哗;做這個(gè)項(xiàng)目的目的也是為了進(jìn)一步提升自己的技術(shù)缔刹。給想學(xué)習(xí)Socket的人一個(gè)平臺(tái)
在我學(xué)習(xí)Socket時(shí),我在網(wǎng)上找了一些相關(guān)的文章劣针,比如:
新手入門IM一篇就夠校镐,iOS 即時(shí)通訊,從入門到 “放棄”捺典?等優(yōu)秀的文章鸟廓,對(duì)于一個(gè)不懂Socket聊天的人看了之后都有同樣的困惑:我怎么開始動(dòng)手做呢?是的這些文章要不就是太深無法下手襟己,要不就是太簡(jiǎn)單無法達(dá)到真正工作時(shí)的要求引谜。那么我現(xiàn)在就做了一個(gè)Socket聊天項(xiàng)目,因?yàn)槭?.0.0版本擎浴,所以代碼量非常少员咽,只是把大體的樣子做了出來;相信你從這個(gè)時(shí)候參與項(xiàng)目就能真正的動(dòng)手做了贮预,真正的去實(shí)現(xiàn)一套聊天系統(tǒng)贝室。
尋求幫助
自己做一個(gè)聊天系統(tǒng)工作量是非常龐大的,文本仿吞、語音等消息發(fā)送档玻,黑名單等關(guān)系處理,單聊茫藏、群聊、討論組等聊天類型霹琼;所以我需要借助開源的力量务傲,找到一群愿意一起做的人,我們一起商量枣申、設(shè)計(jì)完成整個(gè)系統(tǒng)售葡。整個(gè)系統(tǒng)分為客戶端(文中簡(jiǎn)稱C端)和服務(wù)器端(文中簡(jiǎn)稱S端)
目前聊天系統(tǒng)主要是兩大類
1:CS,也就是有客戶端和服務(wù)器忠藤;
2:P2P挟伙,沒有服務(wù)器客戶端和客戶端直連。
上面兩個(gè)各有各的優(yōu)勢(shì)模孩,P2P主要做局域網(wǎng)聊天尖阔,比如:飛信和飛鴿傳書等;
CS才是目前的主流榨咐,比如:QQ等介却。
S端做消息轉(zhuǎn)發(fā)和存儲(chǔ),S端沒怎么重點(diǎn)設(shè)計(jì)块茁,我考慮用原本的C來充當(dāng)S(主要是我并不會(huì)Node.js等語言齿坷,如果能找到一個(gè)人用Node.js或者其他語言實(shí)現(xiàn)S當(dāng)然是最好的):
1:語言只是工具桂肌;
2:我們的設(shè)計(jì)重心在C端;
3:能知道服務(wù)器的邏輯能幫我們更好的理解整個(gè)聊天過程永淌;
4:C和S都是我們熟悉的OC語言崎场,并不會(huì)有任何語言障礙;
5:每個(gè)人都擁有完整的系統(tǒng)遂蛀,自己方便調(diào)試與開發(fā)谭跨;
6:與其他開發(fā)人員并不產(chǎn)生依賴,完全獨(dú)立答恶。
如何參與項(xiàng)目
項(xiàng)目1.0.0我已經(jīng)托管到github饺蚊,目前可以這樣參與該項(xiàng)目:1:forkC端和S端的源碼,切換到對(duì)應(yīng)的分支進(jìn)行開發(fā):
1:master是可交互的悬嗓、功能完善的主分支污呼;
2:1.0.0dev、1.0.1dev等都是從master分支克隆的包竹,用來進(jìn)行對(duì)應(yīng)版本新功能的開發(fā)燕酷;
這些分支是由我來創(chuàng)建的,你們開發(fā)時(shí)只需要跟蹤當(dāng)前版本就好了周瞎;
3:每個(gè)開發(fā)版本bug修改苗缩、功能開發(fā)完畢后才可合并到主分支,這是一個(gè)小約定声诸;
4:這里并沒有為修改bug單獨(dú)拉取一個(gè)分支酱讶,考慮到復(fù)雜性,盡量減少外界干擾彼乌。
開發(fā)完成后New Push Request泻肯,我來Agreen;2:進(jìn)群(289092194加群理由寫Sokcet學(xué)習(xí)交流慰照,我的個(gè)人信息里面留的群號(hào)是iOS交流群灶挟,你也可以加)一起討論、設(shè)計(jì)和開發(fā)毒租。當(dāng)然了如果確實(shí)沒有人愿意和我一起開發(fā)稚铣,我一個(gè)人也會(huì)堅(jiān)持下去的。對(duì)于要參與本項(xiàng)目的人墅垮,我有以下的幾點(diǎn)要求:
1:代碼要規(guī)范惕医,規(guī)范文檔我已經(jīng)放在項(xiàng)目中了;
這樣做的目的是為了保持風(fēng)格統(tǒng)一噩斟,讓其他人能快速的看懂曹锨;
2:注釋要全,代碼是給人看的剃允;
3:你應(yīng)具備一定的iOS開發(fā)經(jīng)驗(yàn)沛简;
4:盡量寫出可進(jìn)行單元測(cè)試的代碼齐鲤;
5:盡量站在工程師的角度來寫代碼,性能椒楣、速度和內(nèi)存等可適當(dāng)考慮给郊;
6:追求質(zhì)量不追求速度,公司總在趕進(jìn)度捧灰,在這里你可以盡情展示你的代碼給別人看淆九;
7:抱著欣賞的角度去看別人的代碼。
做聊天系統(tǒng)你應(yīng)該知道的知識(shí)點(diǎn)
TCP/IP協(xié)議族
網(wǎng)際互聯(lián)層還有其他的叫法:網(wǎng)絡(luò)層等炭庙;
網(wǎng)絡(luò)接口層還有其他的叫法:數(shù)據(jù)鏈路層、鏈路層等煌寇;
大家知道分別指哪一層就好了焕蹄。
我們提到TCP/IP一般就指TCP/IP四層模型,TCP只是傳輸層的一個(gè)協(xié)議阀溶,而IP也只是網(wǎng)際互聯(lián)層的一個(gè)協(xié)議腻脏。TCP/IP是一套定義在硬件層面數(shù)據(jù)傳輸?shù)囊?guī)則,定義這樣一套規(guī)則就是為了讓不同類型數(shù)據(jù)經(jīng)過源地址到目標(biāo)地址中各個(gè)傳輸介質(zhì)時(shí)能差別處理并傳輸罷了银锻。
傳輸協(xié)議選TCP還是UDP
這是一個(gè)必然會(huì)遇到的問題永品,我們?cè)谶@里也不用討論了,多年行業(yè)實(shí)踐經(jīng)驗(yàn)已得出結(jié)論:
1:團(tuán)隊(duì)很牛逼击纬,用UDP或者UDP+TCP鼎姐;
2:團(tuán)隊(duì)一般,用TCP更振。
所以我們用TCP作為我們聊天系統(tǒng)的傳輸協(xié)議症见。
TCP、IP協(xié)議
TCP連接時(shí)的三次握手庶柿、四次揮手
這部分請(qǐng)大家看這篇好文村怪,看完之后我來一下總結(jié):
三次握手:當(dāng)我們自己組裝(請(qǐng)注意體會(huì)這個(gè)詞)的TCP段滿足第一次握手格式,發(fā)送到目的地后
浮庐,目的地如果支持TCP連接甚负,那么它就會(huì)回復(fù)你第二次握手的信息,
你收到第二次握手信息再組裝第三次握手格式的TCP段發(fā)送過去就完成了連接操作审残;
四次揮手:道理類似梭域。
為什么需要三次握手和四次揮手請(qǐng)看這里;我們可以像這個(gè)哥們文章一樣自己封裝TCP段模擬三次握手搅轿;當(dāng)然了你也可以了解一下Charles抓取Https原理病涨。
Socket
上面說了我們要用TCP協(xié)議傳遞數(shù)據(jù),就要懂報(bào)介时、段没宾、幀、握手和揮手沸柔,甚至IP分片循衰、超時(shí)重傳和滑動(dòng)窗口等,讓我們自己來實(shí)現(xiàn)就太麻煩了褐澎。Socket已經(jīng)幫我們做好了這一切会钝,我們只需要用Socket提供的接口就可以,具體哪些接口可以看這里工三∏ㄋ幔看了這些你可能還是覺得很麻煩。所以才有了CocoaAsyncSocket這個(gè)三方庫俭正,它對(duì)用戶隱藏了麻煩的的Socket操作奸鬓,提供給用戶面向?qū)ο蟮腛C接口;所以本系統(tǒng)也是選用CocoaAsyncSocket進(jìn)行二次封裝掸读。
Socket自定義協(xié)議到底是什么
我們用CocoaAsyncSocket中的GCDAsyncSocket類就可以使用TCP傳輸數(shù)據(jù)串远、GCDAsyncUdpSocket類就可以使用UDP傳輸數(shù)據(jù)了;也就是傳輸控制層及其以下的協(xié)議我們都不可能去自定義了儿惫,那么我們自定義協(xié)議自然就是定義的用戶層協(xié)議澡罚。
實(shí)現(xiàn)聊天目前已存在的常見用戶層協(xié)議
如果你并不想自定義用戶層協(xié)議,那么你可以從這里進(jìn)行已有用戶層協(xié)議選擇肾请,并直接使用提供的SDK進(jìn)行開發(fā)留搔。聊天系統(tǒng)當(dāng)然要分C和S,前面也說了我們的S也是用C來充當(dāng)?shù)模ㄔ俅握f明一下)铛铁。
本系統(tǒng)協(xié)議內(nèi)容部分講解
這里并不會(huì)講太多隔显,因?yàn)樵创a你可以直接下載下來却妨,現(xiàn)在是1.0.0版本代碼量很少,你可以通過寫好的連接荣月、登錄管呵、心跳流程來理解整個(gè)項(xiàng)目?jī)?nèi)容;如果你說看不懂那么你可能還沒有到學(xué)習(xí)Socket的地步哺窄,可以過段時(shí)間再來理解捐下。
說這句話并沒有任何惡意,因?yàn)槿魏问虑槎际且徊揭徊絹淼模?在什么階段學(xué)習(xí)什么內(nèi)容,強(qiáng)行吸收不能掌握的內(nèi)容只會(huì)適得其反。
包格式定義
發(fā)送數(shù)據(jù)時(shí)比如我們發(fā)送一條數(shù)據(jù)"hello world"秃诵,可能目的地前后收到了兩條數(shù)據(jù)"hello "和"world"请毛,因此我們需要進(jìn)行分包處理悔据;分包部分原因:
1:以太網(wǎng)限制在46-1500字節(jié),1500就是以太網(wǎng)的MTU,超過這個(gè)量,TCP會(huì)為IP數(shù)據(jù)報(bào)設(shè)置偏移量
進(jìn)行分片傳輸档叔,現(xiàn)在一般可允許應(yīng)用層設(shè)置8k(NTFS系)的緩沖區(qū),
8k的數(shù)據(jù)由底層分片蒸绩,而應(yīng)用看來只是一次發(fā)送衙四;
2:路由器等也是可以設(shè)置大小的。
發(fā)送數(shù)據(jù)時(shí)比如我們前后發(fā)送兩條數(shù)據(jù)"hello "和"world"患亿,可能目的地只收到了一條數(shù)據(jù)"hello world"传蹈,因此我們需要進(jìn)行粘包處理;粘包部分原因:
1:TCP為提高傳輸效率步藕,發(fā)送方往往要收集到足夠多的數(shù)據(jù)后才發(fā)送
一段數(shù)據(jù)惦界。若連續(xù)幾次發(fā)送的數(shù)據(jù)都很少,通常TCP會(huì)根據(jù)優(yōu)化算法把這些數(shù)據(jù)合成一包后一次發(fā)送出去咙冗,
這樣接收方就收到了粘包數(shù)據(jù)沾歪。
因?yàn)門CP/IP是盡可能提供可靠傳輸,傳輸過程還是可能會(huì)丟包或者接收到錯(cuò)誤的包雾消,所以我們還需要進(jìn)行錯(cuò)包處理瞬逊。針對(duì)這三種問題我們都可以為將發(fā)送數(shù)據(jù)加一個(gè)頭部進(jìn)行解決,可在項(xiàng)目IMSocketHeader中看到仪或;目前只在包頭用4字節(jié)放了4個(gè)標(biāo)志位,每個(gè)標(biāo)志位占1字節(jié):
1:version放用戶當(dāng)前聊天系統(tǒng)版本士骤,用于后面聊天發(fā)布新功能時(shí)范删,舊聊天版本不啟用部分功能等;
2:magic_num用于確定包是不是一個(gè)我們應(yīng)該處理的包拷肌,目前寫死到旦;
3:command存放命令類型區(qū)分心跳旨巷、登錄等,因?yàn)榘煌蛻舳颂硗⒎?wù)器處理方式有很大不同采呐;
4:body_len內(nèi)容長(zhǎng)度,用來處理粘包和分包以可到一個(gè)完整的包搁骑,
具體處理流程在IMSocketIO中看到斧吐。
為啥要進(jìn)進(jìn)行粘包、分包和錯(cuò)誤包處理:
只是為了得到一個(gè)完整的解析單元而已仲器,也就是得到一個(gè)完整的我們定義的包(頭部+內(nèi)容)煤率。
數(shù)據(jù)格式
通過加包頭我們丟給TCP層的數(shù)據(jù)就變成了:包頭+內(nèi)容;那么我們把內(nèi)容以什么樣的格式發(fā)送出去呢乏冀?
這里我們把要發(fā)送的對(duì)象轉(zhuǎn)成JSON字符串再轉(zhuǎn)成NSData進(jìn)行發(fā)送蝶糯。
并沒有選用Protobuf的原因如下:
1:并不是所有想?yún)⑴c項(xiàng)目的人都能順利安裝、使用Protobuf環(huán)境辆沦;
2:數(shù)據(jù)格式在整個(gè)系統(tǒng)設(shè)計(jì)中并不重要昼捍,它更屬于后期優(yōu)化內(nèi)容;
3:對(duì)于JSON我們更為熟悉肢扯,使用起來更快捷方便妒茬。
部分項(xiàng)目中重要的類
其實(shí)目前項(xiàng)目中的連接、登錄等流程已經(jīng)把工程中所有的類都使用完了鹃彻,你可以順藤摸瓜的去走一遍流程就知道各個(gè)類的作用和意義了郊闯。不過還是有幾個(gè)核心類需要單獨(dú)拿出來提一下。
IMSocketModules
需要接收的消息在本類中注冊(cè)蛛株,收到未注冊(cè)的消息內(nèi)部會(huì)自動(dòng)丟棄团赁;這有點(diǎn)像MQTT了。
IMSocketControl
超時(shí)重傳谨履、心跳欢摄、數(shù)據(jù)加解密、收到未注冊(cè)的消息內(nèi)部會(huì)在本類丟棄笋粟。
服務(wù)器項(xiàng)目簡(jiǎn)單介紹
1:采用基礎(chǔ)工程快速搭建怀挠;
2:項(xiàng)目中主要就是一個(gè)類ChatSocketServer用于接收客戶端的連接、登錄害捕、退出绿淋、心跳等請(qǐng)求,之所以用一個(gè)文件因?yàn)楸鞠到y(tǒng)重心是在客戶端尝盼;
3:因?yàn)榱奶焓仟?dú)立于應(yīng)用模塊的吞滞,所以服務(wù)器不做登錄用戶驗(yàn)證,也就是任何登錄信息合法的客戶端進(jìn)行登錄服務(wù)器都會(huì)與之建立連接;
4:數(shù)據(jù)庫用Realm來緩存消息裁赠;
5:版本迭代內(nèi)容在項(xiàng)目中Readme.md中實(shí)時(shí)更新殿漠。
客戶端項(xiàng)目簡(jiǎn)單介紹
1:采用基礎(chǔ)工程快速搭建;
2:目前應(yīng)用中模擬了3個(gè)死用戶佩捞,后期會(huì)考慮注冊(cè)功能绞幌,但是目前用不到;
3:有一個(gè)簡(jiǎn)單的UITabBarController作為主控制器一忱,里面有三個(gè)界面:會(huì)話莲蜘、好友、我掀潮;目前只在我界面展示了用戶信息菇夸,其他的界面后面慢慢迭代;
4:編寫界面時(shí)采用MVC邏輯與視圖完全分離仪吧,可以看看ChatMineController庄新,這也是參照Andoird來寫界面,因?yàn)楸旧砦乙彩且粋€(gè)Android開發(fā)者薯鼠,最近打算試試這樣開發(fā)界面的可行性择诈,當(dāng)然了你參與項(xiàng)目時(shí)你可以在你負(fù)責(zé)的模塊中使用MVP、MVVM等出皇;
5:數(shù)據(jù)庫用Realm羞芍,這樣做可以實(shí)現(xiàn)數(shù)據(jù)庫驅(qū)動(dòng)界面實(shí)時(shí)更新,主要是用了Realm的addNotificationBlock方法郊艘;
6:在SocketIMDemoTests中創(chuàng)建和主工程對(duì)應(yīng)文件夾對(duì)應(yīng)文件進(jìn)行單元測(cè)試荷科;
7:項(xiàng)目使用組件化;
8:版本迭代內(nèi)容在項(xiàng)目中Readme.md中實(shí)時(shí)更新纱注。
文末
我從準(zhǔn)備項(xiàng)目到寫完這篇文章不知不覺過去了半個(gè)月畏浆,這一切都是值得的,至少目前我更了解TCP/IP協(xié)議了狞贱。歡迎大家加群和fork項(xiàng)目刻获,不管你是大牛還是想學(xué)習(xí)的小牛,我們都?xì)g迎你的到來瞎嬉,千萬不要低估了自己的力量蝎毡。最壞的情況就是到最后(初步定于2018年3月底開始做1.0.1dev版本)還是只有我一個(gè)人,那么我還是會(huì)堅(jiān)持寫下去的氧枣,只是迭代周期會(huì)長(zhǎng)很多沐兵。
問題反饋區(qū)
在項(xiàng)目發(fā)起后一周,已經(jīng)陸續(xù)有20人參與進(jìn)來便监,參與過程中問題也是很多的扎谎,所以專門在這里記錄下來
怎么運(yùn)行項(xiàng)目?
因?yàn)轫?xiàng)目采用的是組件化,設(shè)置的工程依賴簿透,所以你每次拉取代碼后最好都做一下如下的操作:
1:Clean工程;
2:依次編譯Common解藻、IMServer老充、ModelManager、HttpServer和SocketIMDemo螟左。
3:Run工程啡浊。
運(yùn)行時(shí)Realm報(bào)錯(cuò)
你可能會(huì)遇到下面的錯(cuò)誤原因:
所有存入Realm數(shù)據(jù)庫的模型,如果有改動(dòng)都需要進(jìn)行升級(jí)胶背,
考慮到開發(fā)過程中模型一直會(huì)不停的變巷嚣,所以我并沒有寫升級(jí)代碼。
解決方案:
卸載重裝應(yīng)用就好了钳吟。