iOS 網(wǎng)絡(luò)編程socket

一、概念

首先,理清一些概念

TCP/IP和UDP,HTTP協(xié)議,Socket


1.TCP/IP和UDP,是網(wǎng)絡(luò)中比較底層的協(xié)議,平時(shí)的網(wǎng)絡(luò)連接基本都離不開(kāi)這兩個(gè)協(xié)議,其中

1) TCP:面向連接的傳輸控制協(xié)議:

建立連接,形成傳輸數(shù)據(jù)的通道(建立連接的三次握手,斷開(kāi)連接的四次握手)。

在連接中進(jìn)行大數(shù)據(jù)傳輸,數(shù)據(jù)大小不受限制。

通過(guò)三次握手完成連接,是可靠協(xié)議,數(shù)據(jù)安全送達(dá)。

必須建立連接,效率會(huì)稍低。

2) UDP:面向無(wú)連接的用戶數(shù)據(jù)報(bào)協(xié)議:

只管發(fā)送,不確認(rèn)對(duì)方是否接收到。

不需要建立連接,將數(shù)據(jù)及源和目的封裝成數(shù)據(jù)包中,每個(gè)數(shù)據(jù)報(bào)的大小限制在 64K 之內(nèi)。

因?yàn)闊o(wú)需連接,因此是不可靠協(xié)議。

不需要建立連接,速度快。

應(yīng)用場(chǎng)景:多媒體教室/網(wǎng)絡(luò)流媒體。

2.HTTP 協(xié)議即超文本傳送協(xié)議,是建立在 TCP /IP 協(xié)議之上的一種應(yīng)用,主要解決如何包裝數(shù)據(jù)。

HTTP協(xié)議詳細(xì)規(guī)定了瀏覽器與服務(wù)器之間相互通信的規(guī)則,是萬(wàn)維網(wǎng)交換信息的基礎(chǔ)。

HTTP是基于請(qǐng)求-響應(yīng)形式并且是短連接,并且是無(wú)狀態(tài)的協(xié)議。

HTTP 連接最顯著的特點(diǎn)是客戶端發(fā)送的每次請(qǐng)求都需要服務(wù)器回送響應(yīng),在請(qǐng)求結(jié)束后,會(huì)主動(dòng)釋放連接。從建立連接到關(guān)閉連接的過(guò)程稱為 “一次連接”。

3.Socket通常被稱為“套接字”,通常用于描述IP地址和端口,是一個(gè)通信的句柄,可以用來(lái)實(shí)現(xiàn)不同虛擬機(jī)或者不同計(jì)算機(jī)之間的通信。

Socket連接是長(zhǎng)連接,理論上客戶端和服務(wù)器端一旦建立連接將不會(huì)主動(dòng)斷開(kāi)此連接,表現(xiàn)為持續(xù)連接,服務(wù)端可主動(dòng)將消息推送給客戶端。

Socket雖然能夠?qū)崿F(xiàn)網(wǎng)絡(luò)通信,但實(shí)際上只是對(duì) TCP/IP 協(xié)議的封裝,本身并不是協(xié)議,而是一個(gè)調(diào)用接口(API),是應(yīng)?層與運(yùn)輸層 TCP/IP 協(xié)議族通信的中間軟件抽象層,通過(guò)調(diào)用socket的接口,我們就能使用TCP/IP協(xié)議。

Socket區(qū)別于HTTP協(xié)議的網(wǎng)絡(luò)連接,在 Internet 上的主機(jī)一般運(yùn)行了多個(gè)服務(wù)軟件,同時(shí)提供幾種服務(wù)。每種服務(wù)都打開(kāi)一個(gè) Socket,并綁定到一個(gè)端口上,不同的端口對(duì)應(yīng)于不同的服務(wù)。以網(wǎng)絡(luò)中的兩個(gè)主機(jī)為例,通過(guò)服務(wù)端主機(jī)的IP地址和服務(wù)軟件的端口建立Socket,在客戶端主機(jī)上建立socket并連接到對(duì)應(yīng)服務(wù)端主機(jī)的Socket,在這兩個(gè)socket中間便是網(wǎng)絡(luò)連接,數(shù)據(jù)在兩個(gè)Socket之間進(jìn)行傳輸。

PS:關(guān)于Socket的類型描述放在最后


二、iOS平臺(tái)的Socket編程

(一)BSDSocket

Socket一開(kāi)始是使用純C編寫(xiě)的,是可以跨平臺(tái)的,UNIX最底層的Socket是基于BSDSocket的,而蘋(píng)果的系統(tǒng)底層便是基于UNIX,因此也支持BSDSocket。BSDSocket主要通過(guò)一些函數(shù)實(shí)現(xiàn)Socket連接和數(shù)據(jù)的讀寫(xiě),swift和OC同樣可以使用這些方法,只要import了相應(yīng)的頭文件,但是要實(shí)現(xiàn)跨平臺(tái)的Socket,底層最好還是直接使用C/C++來(lái)實(shí)現(xiàn)Socket連接

BSDSocket最基本的幾個(gè)接口函數(shù)如下:

1)創(chuàng)建Socket并指定使用的協(xié)議(TCP或UDP)

int server = socket(int32 addressFamily, int32 type,int32 protocol)

在創(chuàng)建成功之后,可通過(guò)這個(gè)函數(shù)設(shè)置Socket 的選項(xiàng),即socket的屬性,包括端口重用,收發(fā)數(shù)據(jù)時(shí)延等。

int err= setsockopt(SOCKET s,int level,int optname,const char* optval,int optlen);

2)將主機(jī)的IP地址和某個(gè)端口綁定到Socket

int err=bind(int socketFileDescriptor,sockaddr *addressToBind,int addressStructLength)

PS:以上兩個(gè)函數(shù)是建立socket連接必不可少 的,無(wú)論是TCP還是UDP的C/S(即服務(wù)端和客戶端)程序架構(gòu)的socket連接都要先通過(guò)以上兩步建立Socket。

3)在綁定好之后,根據(jù)選定的協(xié)議,若是UDP,則可以直接進(jìn)行數(shù)據(jù)傳輸,以下是發(fā)送和接收接口

int err=sendto(int socketFileDescriptor,char *buffer, int bufferLength, intflags, sockaddr *destinationAddress, int destinationAddressLength)

int err=recvfrom(int socketFileDescriptor,char *buffer, int bufferLength, intflags, sockaddr *fromAddress, int*fromAddressLength)

4)若是TCP協(xié)議,則服務(wù)端還要進(jìn)行監(jiān)聽(tīng)客戶端連接請(qǐng)求,連接客戶端之后,才能進(jìn)行數(shù)據(jù)傳輸

服務(wù)端

監(jiān)聽(tīng)連接請(qǐng)求

int err=listen(int socketFileDescriptor, int backlogSize)

socket() 函數(shù)創(chuàng)建的 socket 默認(rèn)是一個(gè)主動(dòng)類型的,listen() 函數(shù)將 socket 變?yōu)楸粍?dòng)類型的,等待客戶的連接請(qǐng)求。

接收連接請(qǐng)求

int peerSocket = accept(int socketFielDescriptor, sockaddr *clientAddress, int clientAddressStructLength)

數(shù)據(jù)傳輸

發(fā)送,接收接口,第一個(gè)參數(shù)都是客戶端的socket文件描述符peerSocket

int len=send(int socketFileDescriptor, char*buffer, int bufferLength, int flags)

int len=recv(int socketFileDescriptor, char*buffer, int bufferLength, int flags)

最后關(guān)閉連接

int?close(int socketFileDescriptor)

客戶端

不需要監(jiān)聽(tīng),直接連接到指定的服務(wù)器IP和端口?

客戶端不需要指定打開(kāi)的端口,通常臨時(shí)的、動(dòng)態(tài)的分配一個(gè)1024以上的端口。

int err =?connect(int socketFileDescriptor,sockaddr *serverAddress, int serverAddressLength)

數(shù)據(jù)傳輸

發(fā)送,接收接口,第一個(gè)參數(shù)都是客戶端的socket文件描述符

int len=send(int socketFileDescriptor, char*buffer, int bufferLength, int flags)

int len=recv(int socketFileDescriptor, char*buffer, int bufferLength, int flags)

最后關(guān)閉連接

int?close(int socketFileDescriptor)


注意

這里的accept(),recv(),send(),recvfrom(),sendto()幾個(gè)接口函數(shù)都是阻塞式的,即運(yùn)行時(shí)會(huì)阻塞當(dāng)前線程,因此實(shí)際開(kāi)發(fā)中需要另外開(kāi)辟線程執(zhí)行這些函數(shù)。根據(jù)執(zhí)行結(jié)果再回到主線程進(jìn)行數(shù)據(jù)更新。

完整的UDP連接示意圖


完整的TCP連接示意圖


(二)CFSocket


iOS官方給出的CFSocket,存在于CoreFoundation框架中,使用純C語(yǔ)言實(shí)現(xiàn),同樣可以實(shí)現(xiàn)跨平臺(tái)應(yīng)用,它是基于BSDSocket進(jìn)行抽象和封裝的,CFSocket 中包含了少數(shù)開(kāi)銷,它幾乎可以提供 BSD sockets 所具有的一切功能,并且把 socket 集成進(jìn)一個(gè)“運(yùn)行循環(huán)”RunLoop中。

Core Foundation框架是一組C語(yǔ)言接口,它們?yōu)閕OS應(yīng)用程序提供基本數(shù)據(jù)管理和服務(wù)功能

群體數(shù)據(jù)類型 (數(shù)組、集合等)、程序包、字符串管理、日期和時(shí)間管理、原始數(shù)據(jù)塊管理、偏好管理、URL及數(shù)據(jù)流操作、線程和RunLoop、端口和soket通訊

RunLoop 實(shí)際上就是一個(gè)對(duì)象,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面事件循環(huán)的邏輯。線程執(zhí)行了這個(gè)函數(shù)后,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個(gè)循環(huán)結(jié)束,函數(shù)返回。




CFSocket實(shí)現(xiàn)流式Socket(基于TCP)的過(guò)程如下:(CFSocekt 用于建立連接,CFStream 用于讀寫(xiě)數(shù)據(jù))

這里我使用OC實(shí)現(xiàn),用swift還需要轉(zhuǎn)化指針,代碼過(guò)于復(fù)雜。

服務(wù)端

1)創(chuàng)建socket,這里

CFSocketRef socket_server = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, kCFSocketAcceptCallBack//觸發(fā)類型,TCPServerAcceptCallBack,NULL);

TCPServerAcceptCallBack是一個(gè)回調(diào)函數(shù),在參數(shù)中觸發(fā)類型代表的事件活躍時(shí)觸發(fā)

具體的回調(diào)事件觸發(fā)類型enumCFSocketCallBackType{?

kCFSocketNoCallBack =0,//表示不需要回調(diào)函數(shù),回調(diào)函數(shù)的參數(shù)直接設(shè)置為NULL

kCFSocketReadCallBack =1,? kCFSocketAcceptCallBack =2,(常用)? kCFSocketDataCallBack =3,? kCFSocketConnectCallBack =4,? kCFSocketWriteCallBack =8};

根據(jù)需要設(shè)置端口重用

_Bool reused = YES;

setsockopt(CFSocketGetNative(_socket_server), SOL_SOCKET, SO_REUSEADDR, (const void *)&reused, sizeof(reused));

2)綁定本地IP和端口到Socket

struct sockaddr_in Socaddr;

memset(&Socaddr, 0, sizeof(Socaddr));

Socaddr.sin_len=sizeof(Socaddr);

Socaddr.sin_family=AF_INET;

Socaddr.sin_addr.s_addr=INADDR_ANY;

Socaddr.sin_port=CFSwapInt16(1235);//設(shè)置端口,這里任意取的

CFDataRef dataaddr=CFDataCreate(kCFAllocatorDefault, (UInt8*)&Socaddr, sizeof(Socaddr));

CFSocketError err=CFSocketSetAddress(_socket_server, dataaddr);

CFRelease(dataaddr);

由于Core Foundation的對(duì)象不屬于ARC的管理范疇,所以需要自己release。

3)獲取當(dāng)前線程的runloop并把服務(wù)端Socket加入到RunLoop中,啟動(dòng)RunLoop。

這里實(shí)際上封裝了listen和accept,因此同樣會(huì)產(chǎn)生阻塞,需要另外開(kāi)辟線程。

CFRunLoopRef runLoopRef = CFRunLoopGetCurrent();

CFRunLoopSourceRef sourceRef = CFSocketCreateRunLoopSource(kCFAllocatorDefault, _socket_server, 0);

CFRunLoopAddSource(runLoopRef, sourceRef, kCFRunLoopCommonModes);

CFRelease(sourceRef);

CFRunLoopRun();

如上面1)中所述,回調(diào)函數(shù)TCPServerAcceptCallBack會(huì)在某些事件觸發(fā)時(shí)自動(dòng)執(zhí)行,這里我們要自己設(shè)計(jì)函數(shù)體,在客戶端接入之后進(jìn)行相應(yīng)操作,回調(diào)函數(shù)的參數(shù)是有固定要求的,但函數(shù)名可自定義,具體如下:

void TCPServerAcceptCallBack(CFSocketRef socket,CFSocketCallBackType type,CFDataRef address,const void *data,void *info)

根據(jù)參數(shù)type是否等于kCFSocketAcceptCallBack,即可確定是客戶端連接成功

根據(jù)參數(shù)data此時(shí)是CFSocketNativeHandle類型,獲取客戶端Socket的Handle,再用getpeername獲取客戶端Socket地址

CFSocketNativeHandle nativeHanldle = *(CFSocketNativeHandle*)data;//*去引用

getpeername(nativeHanldle, (struct sockaddr * )name , &namelen)

創(chuàng)建對(duì)應(yīng)客戶端socket的輸入輸出流

void CFStreamCreatePairWithSocket(CFAllocatorRef alloc, CFSocketNativeHandle sock, CFReadStreamRef *readStream, CFWriteStreamRef *writeStream);

CFStreamCreatePairWithSocket()操作成功后,readStream和writeStream都指向有效的地址,因此判斷是不是還是之前設(shè)置的NULL,就知道是否創(chuàng)建成功

客戶端

客戶端相對(duì)簡(jiǎn)單很多,直接創(chuàng)建對(duì)應(yīng)服務(wù)端的IP和端口的Socket,并創(chuàng)建對(duì)應(yīng)的輸入輸出流

NSString * server = @"192.168.8.39";

CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)server, 12435, &readStream, &writeStream);

判斷此時(shí)的readStream和writeStream是否都不等于NULL,若是則創(chuàng)建成功


后續(xù)的流的操作,客戶端和服務(wù)端是相同的


流管理


首先我們要對(duì)NSStream熟悉一下,它是屬于Cocoa框架中的流對(duì)象,Cocoa是蘋(píng)果的面向?qū)ο箝_(kāi)發(fā)框架,Cocoa中的流對(duì)象與Core Foundation中的流對(duì)象是對(duì)應(yīng)的。因此我們可以通過(guò)toll-free橋接方法來(lái)進(jìn)行相互轉(zhuǎn)換。NSStream、NSInputStream和NSOutputStream分別對(duì)應(yīng)CFStream、CFReadStream和CFWriteStream。

我們要做的就是繼承協(xié)議NSStreamDelegate,將CFStream轉(zhuǎn)化為對(duì)應(yīng)的NSStream并加入到RunLoop,打開(kāi)流

NSInoutstream* instream;

NSOutputStream * outstream;

self.instream = (__bridge NSInputStream*)readStreamRef;

self.outstream = (__bridge NSOutputStream*) writeStreamRef;

[self.instream setDelegate:selfClass];

[self.outstream setDelegate: selfClass];

[self.instream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

[self.outstream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

[self.instream open];

[self.outstream open];

實(shí)現(xiàn)協(xié)議方法

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode

在這個(gè)函數(shù)中可以根據(jù)下面??數(shù)據(jù)流的狀態(tài)枚舉進(jìn)行不同的流操作

NSStreamEventNone? ? ? ? ? ? ? ? ? // 無(wú)事件

NSStreamEventOpenCompleted? ? ? ? // 建立連接完成

NSStreamEventHasBytesAvailable? ? // 有可讀的字節(jié),接收到了數(shù)據(jù),可以讀了

NSStreamEventHasSpaceAvailable? ? // 可以使用輸出流的空間,此時(shí)可以發(fā)送數(shù)據(jù)給服務(wù)器

NSStreamEventErrorOccurred? ? ? ? // 發(fā)生錯(cuò)誤

NSStreamEventEndEncountered? ? ? ? // 流結(jié)束事件,在此事件中負(fù)責(zé)做銷毀工作

輸入流的讀

[_instream read:(nonnull uint8_t *) maxLength:(NSUInteger)]

輸出流的寫(xiě)

[_outstream write:(nonnull const uint8_t *) maxLength:(NSUInteger)]

通過(guò)服務(wù)端和客戶端各自的讀寫(xiě)流,就能夠相互傳輸數(shù)據(jù)了,文件圖片等都可以轉(zhuǎn)化成字節(jié)流進(jìn)行傳輸,這里就不多說(shuō)了。


除了以上兩種方法,有很多第三方的庫(kù)都能實(shí)現(xiàn)Socket,操作會(huì)更加簡(jiǎn)單 ,如GCDAsyncSocket,webSocket,Socket.IO


附錄:

Socket的類型

流套接字(SOCK_STREAM):流套接字用于提供面向連接、可靠的數(shù)據(jù)傳輸服務(wù)。該服務(wù)將保證數(shù)據(jù)能夠?qū)崿F(xiàn)無(wú)差錯(cuò)、無(wú)重復(fù)發(fā)送,并按順序接收。流套接字之所以能夠?qū)崿F(xiàn)可靠的數(shù)據(jù)服務(wù),原因在于其使用了傳輸控制協(xié)議,即TCP(The Transmission Control Protocol)協(xié)議。

數(shù)據(jù)報(bào)套接字(SOCK_DGRAM):數(shù)據(jù)報(bào)套接字提供了一種無(wú)連接的服務(wù)。該服務(wù)并不能保證數(shù)據(jù)傳輸?shù)目煽啃裕瑪?shù)據(jù)有可能在傳輸過(guò)程中丟失或出現(xiàn)數(shù)據(jù)重復(fù),且無(wú)法保證順序地接收到數(shù)據(jù)。數(shù)據(jù)報(bào)套接字使用UDP(User Datagram Protocol)協(xié)議進(jìn)行數(shù)據(jù)的傳輸。由于數(shù)據(jù)包套接字不能保證數(shù)據(jù)傳輸?shù)目煽啃裕瑢?duì)于有可能出現(xiàn)的數(shù)據(jù)丟失情況,需要在程序中做相應(yīng)的處理。

原始套接字(SOCK_RAW):原始套接字與標(biāo)準(zhǔn)套接字(標(biāo)準(zhǔn)套接字指的是前面介紹的流套接字和數(shù)據(jù)報(bào)套接字)的區(qū)別在于:原始套接字可以讀寫(xiě)內(nèi)核沒(méi)有處理的IP數(shù)據(jù)包,而流套接字只能讀取TCP協(xié)議的數(shù)據(jù),數(shù)據(jù)報(bào)套接字只能讀取UDP協(xié)議的數(shù)據(jù)。因此,如果要訪問(wèn)其他協(xié)議發(fā)送數(shù)據(jù)必須使用原始套接字。


名字/常量 描述

SOCK_STREAM 這個(gè)協(xié)議是按照順序的、可靠的、數(shù)據(jù)完整的基于字節(jié)流的連接。這是一個(gè)使用最多的socket類型,這個(gè)socket是使用TCP來(lái)進(jìn)行傳輸。

SOCK_DGRAM 這個(gè)協(xié)議是無(wú)連接的、固定長(zhǎng)度的傳輸調(diào)用。該協(xié)議是不可靠的,使用UDP來(lái)進(jìn)行它的連接。

SOCK_SEQPACKET 這個(gè)協(xié)議是雙線路的、可靠的連接,發(fā)送固定長(zhǎng)度的數(shù)據(jù)包進(jìn)行傳輸。必須把這個(gè)包完整的接受才能進(jìn)行讀取。

SOCK_RAW 這個(gè)socket類型提供單一的網(wǎng)絡(luò)訪問(wèn),這個(gè)socket類型使用ICMP公共協(xié)議。(ping、traceroute使用該協(xié)議)

SOCK_RDM 這個(gè)類型是很少使用的,在大部分的操作系統(tǒng)上沒(méi)有實(shí)現(xiàn),它是提供給數(shù)據(jù)鏈路層使用,不保證數(shù)據(jù)包的順序

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 一、網(wǎng)絡(luò)各個(gè)協(xié)議:TCP/IP、SOCKET、HTTP等 網(wǎng)絡(luò)七層由下往上分別為物理層、數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層...
    杯水救車薪閱讀 2,300評(píng)論 0 17
  • 2017.910,今天不是911.早上7點(diǎn)送寶坐車,結(jié)果等車的時(shí)候來(lái)個(gè)女司機(jī),問(wèn)我們走不,寶猶豫了下就上車了,我則...
    艷兒吖閱讀 175評(píng)論 0 0
  • 我們?cè)谶@里盤點(diǎn)的學(xué)校與普通學(xué)校不同之處在于他們都是虛擬的,但虛擬學(xué)校會(huì)讓你的學(xué)習(xí)比現(xiàn)實(shí)學(xué)習(xí)更加有趣,并教導(dǎo)出眾多英...
    f2bc274c0ca8閱讀 500評(píng)論 0 2