iOS開發之網路程式設計:Socket網路程式設計
一、Socket原理
- 套接字(socket)概念
套接字(socket)是通訊的基石,是支援TCP/IP協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊:連線使用的協議,本地主機的IP地址,本地程序的協議埠,遠地主機的IP地址,遠地程序的協議埠。
應用層通過傳輸層進行資料通訊時,TCP會遇到同時為多個應用程式程序提供併發服務的問題。多個TCP連線或多個應用程式程序可能需要通過同一個 TCP協議埠傳輸資料。為了區別不同的應用程式程序和連線,許多計算機作業系統為應用程式與TCP/IP協議互動提供了套接字(Socket)介面。應用層可以和傳輸層通過Socket介面,區分來自不同應用程式程序或網路連線的通訊,實現資料傳輸的併發服務。
- 建立socket連線
建立Socket連線至少需要一對套接字,其中一個運行於客戶端,稱為ClientSocket,另一個運行於伺服器端,稱為ServerSocket。
套接字之間的連線過程分為三個步驟:伺服器監聽,客戶端請求,連線確認。
伺服器監聽:伺服器端套接字並不定位具體的客戶端套接字,而是處於等待連線的狀態,實時監控網路狀態,等待客戶端的連線請求。
客戶端請求:指客戶端的套接字提出連線請求,要連線的目標是伺服器端的套接字。為此,客戶端的套接字必須首先描述它要連線的伺服器的套接字,指出伺服器端套接字的地址和埠號,然後就向伺服器端套接字提出連線請求。
連線確認:當伺服器端套接字監聽到或者說接收到客戶端套接字的連線請求時,就響應客戶端套接字的請求,建立一個新的執行緒,把伺服器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式建立連線。而伺服器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連線請求。
- socket連線與TCP連線
建立Socket連線時,可以指定使用的傳輸層協議,Socket可以支援不同的傳輸層協議(TCP或UDP),當使用TCP協議進行連線時,該Socket連線就是一個TCP連線。
- Socket連線與HTTP連線
由於通常情況下Socket連線就是TCP連線,因此Socket連線一旦建立,通訊雙方即可開始相互發送資料內容,直到雙方連線斷開。但在實際網路應用中,客戶端到伺服器之間的通訊往往需要穿越多箇中間節點,例如路由器、閘道器、防火牆等,大部分防火牆預設會關閉長時間處於非活躍狀態的連線而導致 Socket 連線斷連,因此需要通過輪詢告訴網路,該連線處於活躍狀態。
而HTTP連線使用的是“請求—響應”的方式,不僅在請求時需要先建立連線,而且需要客戶端向伺服器發出請求後,伺服器端才能回覆資料。
很多情況下,需要伺服器端主動向客戶端推送資料,保持客戶端與伺服器資料的實時與同步。此時若雙方建立的是Socket連線,伺服器就可以直接將資料傳送給客戶端;若雙方建立的是HTTP連線,則伺服器需要等到客戶端傳送一次請求後才能將資料傳回給客戶端,因此,客戶端定時向伺服器端傳送連線請求,不僅可以保持線上,同時也是在“詢問”伺服器是否有新的資料,如果有就將資料傳給客戶端。
二、Socket實現即時通訊
首先我們使用的Socket不是官方原生的,而是用的第三方在原生Socket基礎上封裝的— —AsyncSocket。
下面是運行於客戶端的Socket——ClientSocket:
- // 初始化Socket,設定代理
- _socket =[[AsyncSocket alloc] initWithDelegate:self];
- // 連線到主機
- if(![_socket connectToHost:_clientIPAddress.text onPort:[_clientPort.text intValue] error:&error]){
- // 連線失敗
- _status.text =@"Could't Connect Server";
- }else{
- // 連線成功
- _status.text =@"Connect Server Successfully";
- }
- // 傳送資料到Socket,傳送完成會呼叫代理方法
- [_socket writeData:[_sendMessage.text dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0];
- // 從Socket讀取第一個可用資料,如果timeout的值為負數,則讀取操作不會存在超時
- [_socket readDataWithTimeout:-1 tag:0];
- #pragma mark AsyncSocketDelegate代理方法
- // 建立連線
- -(void)onSocket:(AsyncSocket*)sock didConnectToHost:(NSString*)host port:(UInt16)port {
- [self append:[NSString stringWithFormat:@"ConnectToHost:%@", host]];
- [sock readDataWithTimeout:-1 tag:0];
- }
- // Socket完成寫入請求資料後呼叫
- -(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag {
- [sock readDataWithTimeout:-1 tag:0];
- }
- // 讀取資料
- -(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag {
- NSString*newMessage =[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- [self append:[NSString stringWithFormat:@"%@:%@", sock.connectedHost, newMessage]];
- [_socket readDataWithTimeout:-1 tag:0];//可以省略
- }
- // 是否加密
- -(void)onSocketDidSecure:(AsyncSocket*)sock {
- NSLog(@"onSocket:%p", sock);
- }
- // 遇到錯誤時斷開連線
- -(void)onSocket:(AsyncSocket*)sock willDisconnectWithError:(NSError*)err {
- [self append:[NSString stringWithFormat:@"onSocket:%p, error:%@", sock, err.localizedDescription]];
- }
- // 斷開連線
- -(void)onSocketDidDisconnect:(AsyncSocket*)sock {
- NSString*msg =@"Connect is disconnect";
- _status.text = msg;
- _socket =nil;
- }
由於我這邊沒有可用的主機地址,所以只能模擬一下。
下面是運行於伺服器端的Socket——ServerSocket:
- // 初始化Socket監聽
- listenerSocket =[[AsyncSocket alloc] initWithDelegate:self];
- // 開始監聽,接受所給埠的連線
- if(![listenerSocket acceptOnPort:_SERVER_PORT_ error:&error]){
- return;
- }
- // 斷開所有connectionSocket
- for(int i =0; i < connnectionSocketsArray.count; i++){
- [[connnectionSocketsArray objectAtIndex:i] disconnect];
- }
- // 給當前監聽的Socket傳送資料
- [listenerSocket writeData:[_sendMessage.text dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:1];
- #pragma mark socketDeleaget 代理方法
- // 連線socket出錯
- -(void)onSocket:(AsyncSocket*)sock willDisconnectWithError:(NSError*)err {
- NSLog(@"error:%@", err.localizedDescription);
- }
- // 收到新的socket連線
- -(void)onSocket:(AsyncSocket*)sock didAcceptNewSocket:(AsyncSocket*)newSocket {
- [connnectionSocketsArray addObject:newSocket];
- }
- // 讀取資料
- -(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag {
- // 從Socket獲取第一個可獲取的位元組資料
- [sock readDataWithTimeout:-1 tag:0];
- }
- // 與伺服器建立連線,併發送訊息
- -(void)onSocket:(AsyncSocket*)sock didConnectToHost:(NSString*)host port:(UInt16)port {
- NSLog(@"host:%@", host);
- NSString*msg =@"Welcome To Socket Test Server";
- NSData*data =[msg dataUsingEncoding:NSUTF8StringEncoding];
- // 給Socket一個連線成功的訊息
- [sock writeData:data withTimeout:-1 tag:0];
- }
- // 讀取客戶端發來的資料
- -(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag {
- NSString*msg =[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
- // 假設當前有2個ip(10.128.18.157; 10.128.18.158)相互發送訊息
- NSString*ip =@"10.128.18.159";
- // 獲取當前的socket
- for(int i =0; i < connnectionSocketsArray.count; i++){
- AsyncSocket*socket =(AsyncSocket*)[connnectionSocketsArray objectAtIndex:i];
- if([socket.connectedHost isEqualToString:ip]){
- // 找到所要傳送訊息的目標Socket,傳送過去
- [socket writeData:data withTimeout:-1 tag:0];
- // 記錄接收的資訊
- if(msg){
- [self append:msg];
- NSLog(@"%@", msg);
- }else{
- NSLog(@"error!");
- }
- }
- // 客戶端未連線
- else{
- NSString*returnMsg =@"The Other Is Not Online";
- NSData*returnData =[returnMsg dataUsingEncoding:NSUTF8StringEncoding];
- [sock writeData:returnData withTimeout:-1 tag:0];
- }
- }
- }
- // 斷開
- -(void)onSocketDidDisconnect:(AsyncSocket*)sock {
- [connnectionSocketsArray removeObject:sock];
- }