前言
我是培訓班出身的,我至今還記得老師關于socket的一句話:http是短連接,socket是長連接。我估計是老師對我們這群菜鳥不報什么希望,所以才這么說的,而我直到前一陣子還一直當真理相信著。。。
最近工作上接觸了socket,看了很多文檔,漸漸的對socket有了一個清晰的了解,下面附上2個比較好的連接:
- https://cainluo.github.io/14986613643920.html
-
http://www.cocoachina.com/ios/20180228/22385.html
你還可以看文章中的鏈接,也是好文章。
在這個充斥著互聯網的世界,單機的APP已經漸漸銷聲匿跡,網絡編程成為了一個程序員的基本素養。在此,我推薦2本我準備要看的書給和我一樣非科班出身的程序猿:《計算機網絡-自頂向下方法》,還有就是《TCP-IP詳解》的3卷。這2本有先后順序,先讀第一本,在理解第二本會好很多。書單鏈接:
- 鏈接: https://pan.baidu.com/s/1cj4tJX0qG0yDXLI6Gy6mLA 密碼: 1y8i
- 鏈接: https://pan.baidu.com/s/1uZcREgN08tU1Sd2G8kRP8g 密碼: xw2q
與君共勉
socket框架
我用的是GCDAsyncSocket
,畢竟對c的api一臉懵逼的,所以找一個成名的、封裝好、面向對象的socket框架,GCDAsyncSocket
的用法我就不多說了,自己可以百度。
大家可以去https://github.com/robbiehanson/CocoaAsyncSocket下載,當然,也可以下我的demo直接拿。注意到圖片里的udp了么,我們用的是tcp協議的,后面可以帶大家看3次握手和4次分手的過程。
socket客戶端
首先在storyboard里拖個小界面,在ViewController
里關聯下,接著聲明一個GCDAsyncSocket
的對象,如果不持有屬性的話,對象釋放的時候會自動斷開連接。
@property (nonatomic, strong) GCDAsyncSocket *socket;
創建socket,這邊GCDAsyncSocket
用法詳解就不說了,自己百度。
_socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
給button加個點擊事件,里面有連接服務器,和發送數據事件的切換。值得注意的是,每個發送的消息必須要有特定字符分隔開,不然后臺無法識別數據是否已經發送完成,常用的是換行符。[GCDAsyncSocket CRLFData]
框架里已經封裝給我們了。所以每次發送的數據都要拼接上[GCDAsyncSocket CRLFData]
if (self.button.tag == SendType_Connent) {
NSArray *arr = [self.textField.text componentsSeparatedByString:@":"];
NSError *error;
[self.socket connectToHost:arr.firstObject onPort:[arr.lastObject intValue] withTimeout:15 error:&error];
if (error) {
NSLog(@"%@, %d", error, __LINE__);
}
}else {
NSMutableData *data = [self.textField.text dataUsingEncoding:NSUTF8StringEncoding].mutableCopy;
[data appendData: [GCDAsyncSocket CRLFData]];
[self.socket writeData:data withTimeout:30 tag:0];
}
接著你可以把GCDAsyncSocketDelegate
中的所有代理都拷貝過來,方便自己學習,你可以在所有代理方法中加上這句,這樣就很方便就看到那些代理方法調用了。
NSLog(@"%s,%d", __func__, __LINE__);
下面是代理方法的書寫
首先是連上服務器的回調,連上的時候把button的事件改成發送,然后監聽服務器的數據。
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port{
NSLog(@"%s,%d", __func__, __LINE__);
self.button.tag = SendType_SendMessage;
[self.button setTitle:@"發送" forState:UIControlStateNormal];
[self.socket readDataWithTimeout:-1 tag:0];
}
然后是數據監聽的回調,將NSData
轉成NSString
,并在控制臺打印
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"%s,%d", __func__, __LINE__);
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
[self.socket readDataWithTimeout:-1 tag:0];
}
最后是斷開連接的回調,把button調回連接狀態。
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err{
NSLog(@"%s,%d", __func__, __LINE__);
self.button.tag = SendType_Connent;
[self.button setTitle:@"連接" forState:UIControlStateNormal];
}
一個簡單demo就完成了,寫完了當然要測試,在終端用netcat工具實現簡單的服務器聊天功能,命令是nc -lk 端口
當光標移到下面去的時候,表示服務器已經開始監聽啦。運行demo,如果是模擬器,那么ip寫上127.0.0.1的回環地址,如果是真機的話,寫上電腦的ip地址就行。端口的話就和服務器監聽的一致就行。輸入ip后,點擊連接,如果成功的話,控制臺會打印成功的回調。
接著在輸入框里可以輸入內容聊天了,比如我輸入一個hello
控制臺便會跳出來一個hello,控制臺也會打印
didWriteDataWithTag
的回調。如果你在終端輸入內容(回車鍵發送),那么你也會收到信息你關掉終端或者按ctrl+c
便能關掉服務端,會收到socketDidDisconnect
的回調。
客戶端的小demo就完成啦。
socket服務端
手機當服務器,有沒有覺得很有成就感?
服務端的demo很多內容和上面一樣,具體可以看demo中的內容,著重說下不同點。
首先是socket的創建,這個socket對象只負責端口的監聽,并不負責data的傳送。一旦這個socket斷開了連接,其他客戶端就再也連不上這臺服務器了。
self.serverSocket = [[GCDAsyncSocket alloc]initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
//監聽某個端口 等待被鏈接 0-25535 1000以內是系統預留端口
[self.serverSocket acceptOnPort:5555 error:nil];
一旦有客戶端有連接的話,便會回調這個方法:
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket{
NSLog(@"%s,%d", __func__, __LINE__);
//把鏈接進來的socket對象 持有住不被自動釋放 釋放掉的話 鏈接會自動斷開
[self.sockets addObject:newSocket];
[newSocket readDataWithTimeout:-1 tag:0];
}
這里會有一個newSocket
的參數,這個newSocket
對象表示是與當前客戶端的連接,作用是和當前客戶端相互傳送data的,self.serverSocket
的代理對象會賦值給這個newSocket
,所以newSocket
也會走你寫的代理方法。當然,可能會有好幾個客戶端接進來,所以你需要用一個數組來管理,創建數組我就不展示了。
所以走didReadData
回調的是你sockets
數組中的某一個,并不是一開始創建的self.serverSocket
。我為了識別是哪一個客戶端給我發的消失,創建了一個對象指向它:
@property (nonatomic, weak)GCDAsyncSocket *currentSocket;
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag{
NSLog(@"%s,%d", __func__, __LINE__);
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
self.currentSocket = sock;
[sock readDataWithTimeout:-1 tag:0];
}
當有消息進來的時候,我指向那個客戶端,方便給他回消息(暫時不考慮并發的情況,只是demo嘛)
客戶端斷開連接的時候,socket記得從數組中移除。
接下來就是測試啦,先運行demo,然后用終端當客戶端,命令是nc 127.0.0.1 5555
,host和port可以根據自己的實際情況自己改,你可以多開幾個終端,同時連上服務器。
我連了3個,就有3次回調,數組也有3個。data傳送你們自己試吧。
TCP的3次握手和4次分手
我們平時用的抓包工具是Charles,但這個一般用來抓http協議請求的,他幫我們做了很多處理,所以很多細節都看不到,對于socket來說,這工具就不夠看了。推薦一個新工具--Wireshark。
工具的使用自己百度搜吧,我也用的很生疏。
接下來我模擬器運行服務端,真機運行客戶端,模擬器ip是10.10.2.47,真機ip是10.10.2.50
當我點擊連接的時候出現了4條數據,前面3條是不是很熟悉,就是一直念在口中的3次握手,第4條數據是滑動窗口的概念。
然后我客戶端發送了2條后,服務器也發送了1條。由圖可以得到每條消息要2個數據包,來回各一次。
最后斷開連接,是不是很完美的4次分手?
結語
附上代碼地址:https://github.com/harryphone/SocketDemo
用Wireshark可以分解出一次完整的http請求,下次有機會用socket封裝出一個http請求,甚至是https請求。