socket編程是網絡常用的編程,我們通過在網絡中創建socket關鍵字來實現網絡間的通信,通過收集大量的資料,通過這一章節,充分的了解socket編程,文章用引用了大量大神的分析,加上自己的理解,做個總結性的文章
1:socket大致介紹
socket編程是一門技術,它主要是在網絡通信中經常用到
既然是一門技術,由于現在是面向對象的編程,一些計算機行業的大神通過抽象的理念,在現實中通過反復的理論或者實際的推導,提出了抽象的一些通信協議,基于tcp/ip協議,提出大致的構想,一些泛型的程序大牛在這個協議的基礎上,將這些抽象化的理念接口化,針對協議提出的每個理念,專門的編寫制定的接口,與其協議一一對應,形成了現在的socket標準規范,然后將其接口封裝成可以調用的接口,供開發者使用
目前,開發者開發出了很多封裝的類來完善socket編程,都是更加方便的實現剛開始socket通信的各個環節,所以我們首先必須了解socket的通信原理,只有從本質上理解socket的通信,才可能快速方便的理解socket的各個環節,才能從底層上真正的把握
2:TCP/IP協議
要理解socket必須的得理解tcp/ip,它們之間好比送信的線路和驛站的作用,比如要建議送信驛站,必須得了解送信的各個細節。
TCP/IP協議不同于iso的7個分層,它是根據這7個分層,將其重新劃分,好比打掃衛生,本來有掃帚,垃圾斗,抹布,涂料,盆栽等就好比OSI的標準幾個分層,tcp/ip根據用途和功能,將掃帚,垃圾斗放到粗略整理層,抹布涂料放到中度整理層,盆栽放到最終效果層。這里TCP/IP也對OSI的網絡模型層進行了劃分:大致如下:
OSI模型:
TCP/IP協議參考模型把所有的TCP/IP系列協議歸類到四個抽象層中
應用層:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
傳輸層:TCP,UDP
網絡層:IP,ICMP,OSPF,EIGRP,IGMP
數據鏈路層:SLIP,CSLIP,PPP,MTU
每一抽象層建立在低一層提供的服務上,并且為高一層提供服務,看起來大概是這樣子的
通過上面的圖形,由于底一層的需要向高一層的提供服務,我們大致的理解應用程序需要傳輸層的tcp和網絡層的ip協議提供服務,但是我們這章要分析的socket它是在tcpip協議的那一部分呢,就好比,我們的通訊線路已經有明確的規定,我們的驛站要設計在哪個地方一樣
3:回過頭再來理解socket
到目前為止,大致的了解了應用程序和tcpip協議的大致關系,我們只是知道socket編程是在tcp/IP上的網絡編程,但是socket在上述的模型的什么位置呢。這個位置被一個天才的理論家或者是抽象的計算機大神提出并且安排出來
我們可以發現socket就在應用程序的傳輸層和應用層之間,設計了一個socket抽象層,傳輸層的底一層的服務提供給socket抽象層,socket抽象層再提供給應用層,問題又來了,應用層和socket抽象層之間和傳輸層,網絡層之間如何通訊的呢,了解這個之前,我們還是回到原點
要想理解socket編程怎么通過socket關鍵詞實現服務器和客戶端通訊,必須得實現的了解tcp/ip是怎么通訊的,在這個的基礎上在去理解socket的握手通訊
在tcp/ip協議中,tcp通過三次握手建立起一個tcp的鏈接,大致如下
第一次握手:客戶端嘗試連接服務器,向服務器發送syn包,syn=j,客戶端進入SYN_SEND狀態等待服務器確認
第二次握手:服務器接收客戶端syn包并確認(ack=j+1),同時向客戶端發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態
第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手
三次握手如下圖:
根據tcp的三次握手,socket也定義了三次握手,也許是參考tcp的三次握手,一些計算機大神們畫出了socket的三次握手的模型圖
模型圖如下:
在上面圖的基礎上,如果我們得到上面的圖形,需要我們自己開發一些接口,來滿足上面的通訊的三次握手,問題就出來了,我們會需要開發哪些函數
4:socket的一些接口函數原理
通過上面的圖,我們清楚,我們好比一些泛型的程序員,一些理論提供者提供給了我們上面的圖形的理論,我們需要做的就是講上面的圖形的抽象化的東西具體化
第一次握手:客戶端需要發送一個syn j 包,試著去鏈接服務器端,于是客戶端我們需要提供一個鏈接函數
第二次握手:服務器端需要接收客戶端發送過來的syn J+1 包,然后在發送ack包,所以我們需要有服務器端接受處理函數
第三次握手:客戶端的處理函數和服務器端的處理函數
三次握手只是一個數據傳輸的過程,但是,我們傳輸前需要一些準備工作,比如將創建一個套接字,收集一些計算機的資源,將一些資源綁定套接字里面,以及接受和發送數據的函數等等,這些功能接口在一起構成了socket的編程
下面大致的按照客戶端和服務端將所需的函數詳細的列舉出來
上面的兩個圖都概述了socket的通訊原理
5:socket的一個例子. 總結上述的問題
詳細就不在說明,通過一段代碼詳細的解釋
客戶端的代碼:
#include <winsock2.h> #include <stdio.h> #pragma comment(lib,"ws2_32.lib") int main() { //SOCKET前的一些檢查,檢查協議庫的版本,為了避免別的版本的socket,并且通過 //WSAStartup啟動對應的版本,WSAStartup的參數一個是版本信息,一個是一些詳細的細節,注意高低位 //WSAStartup與WSACleanup對應 int err; WORD versionRequired; WSADATA wsaData; versionRequired=MAKEWORD(1,1); err=WSAStartup(versionRequired,&wsaData);//協議庫的版本信息 //通過WSACleanup的返回值來確定socket協議是否啟動 if (!err) { printf("客戶端嵌套字已經打開!\n"); } else { printf("客戶端的嵌套字打開失敗!\n"); return 0;//結束 } //創建socket這個關鍵詞,這里想一下那個圖形中的socket抽象層 //注意socket這個函數,他三個參數定義了socket的所處的系統,socket的類型,以及一些其他信息 SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0); //socket編程中,它定義了一個結構體SOCKADDR_IN來存計算機的一些信息,像socket的系統, //端口號,ip地址等信息,這里存儲的是服務器端的計算機的信息 SOCKADDR_IN clientsock_in; clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); clientsock_in.sin_family=AF_INET; clientsock_in.sin_port=htons(6000); //前期定義了套接字,定義了服務器端的計算機的一些信息存儲在clientsock_in中, //準備工作完成后,然后開始將這個套接字鏈接到遠程的計算機 //也就是第一次握手 connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//開始連接 char receiveBuf[100]; //解釋socket里面的內容 recv(clientSocket,receiveBuf,101,0); printf("%s\n",receiveBuf); //發送socket數據 send(clientSocket,"hello,this is client",strlen("hello,this is client")+1,0); //關閉套接字 closesocket(clientSocket); //關閉服務 WSACleanup(); return 0; }
對應服務端的代碼
#include <winsock2.h> #include <stdio.h> #pragma comment(lib,"ws2_32.lib") int main() { //創建套接字,socket前的一些檢查工作,包括服務的啟動 WORD myVersionRequest; WSADATA wsaData; myVersionRequest=MAKEWORD(1,1); int err; err=WSAStartup(myVersionRequest,&wsaData); if (!err) { printf("已打開套接字\n"); } else { //進一步綁定套接字 printf("嵌套字未打開!"); return 0; } SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//創建了可識別套接字 //需要綁定的參數,主要是本地的socket的一些信息。 SOCKADDR_IN addr; addr.sin_family=AF_INET; addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址 addr.sin_port=htons(6000);//綁定端口 bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//綁定完成 listen(serSocket,5);//其中第二個參數代表能夠接收的最多的連接數 SOCKADDR_IN clientsocket; int len=sizeof(SOCKADDR); while (1) { //第二次握手,通過accept來接受對方的套接字的信息 SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//如果這里不是accept而是conection的話。。就會不斷的監聽 char sendBuf[100]; sprintf(sendBuf,"welcome %s to bejing",inet_ntoa(clientsocket.sin_addr));//找對對應的IP并且將這行字打印到那里 //發送信息 send(serConn,sendBuf,strlen(sendBuf)+1,0); char receiveBuf[100];//接收 recv(serConn,receiveBuf,strlen(receiveBuf)+1,0); printf("%s\n",receiveBuf); closesocket(serConn);//關閉 WSACleanup();//釋放資源的操作 } return 0; }
6:上面例子用到的知識點
(摘抄carter大神文章):
服務器端:
其過程是首先服務器方要先啟動,并根據請求提供相應服務:
(1)打開一通信通道并告知本地主機,它愿意在某一公認地址上的某端口(如FTP的端口可能為21)接收客戶請求;
(2)等待客戶請求到達該端口;
(3)接收到客戶端的服務請求時,處理該請求并發送應答信號。接收到并發服務請求,要激活一新進程來處理這個客戶請求(如UNIX系統中用fork、exec)。新進程處理此客戶請求,并不需要對其它請求作出應答。服務完成后,關閉此新進程與客戶的通信鏈路,并終止。
(4)返回第(2)步,等待另一客戶請求。
(5)關閉服務器
客戶端:
(1)打開一通信通道,并連接到服務器所在主機的特定端口;
(2)向服務器發服務請求報文,等待并接收應答;繼續提出請求......
(3)請求結束后關閉通信通道并終止。
從上面所描述過程可知:
(1)客戶與服務器進程的作用是非對稱的,因此代碼不同。
(2)服務器進程一般是先啟動的。只要系統運行,該服務進程一直存在,直到正常或強迫終止。
7:下面就介紹一些API函數:
(摘抄carter大神文章):
創建套接字──socket()
應用程序在使用套接字前,首先必須擁有一個套接字,系統調用socket()向應用程序提供創建套接字的手段,其調用格式如下:
SOCKET PASCAL FAR socket(int af, int type, int protocol)
該調用要接收三個參數:af、type、protocol。參數af指定通信發生的區域:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中僅支持AF_INET,它是網際網區域。因此,地址族與協議族相同。參數type 描述要建立的套接字的類型。這里分三種:
(1)一是TCP流式套接字(SOCK_STREAM)提供了一個面向連接、可靠的數據傳輸服務,數據無差錯、無重復地發送,且按發送順序接收。內設流量控制,避免數據流超限;數據被看作是字節流,無長度限制。文件傳送協議(FTP)即使用流式套接字。
(2)二是數據報式套接字(SOCK_DGRAM)提供了一個無連接服務。數據包以獨立包形式被發送,不提供無錯保證,數據可能丟失或重復,并且接收順序混亂。網絡文件系統(NFS)使用數據報式套接字。
(3)三是原始式套接字(SOCK_RAW)該接口允許對較低層協議,如IP、ICMP直接訪問。常用于檢驗新的協議實現或訪問現有服務中配置的新設備。
參數protocol說明該套接字使用的特定協議,如果調用者不希望特別指定使用的協議,則置為0,使用默認的連接模式。根據這三個參數建立一個套接字,并將相應的資源分配給它,同時返回一個整型套接字號。因此,socket()系統調用實際上指定了相關五元組中的“協議”這一元。
指定本地地址──bind()
當一個套接字用socket()創建后,存在一個名字空間(地址族),但它沒有被命名。bind()將套接字地址(包括本地主機地址和本地端口地址)與所創建的套接字號聯系起來,即將名字賦予套接字,以指定本地半相關。其調用格式如下:
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
參數s是由socket()調用返回的并且未作連接的套接字描述符(套接字號)。參數name 是賦給套接字s的本地地址(名字),其長度可變,結構隨通信域的不同而不同。namelen表明了name的長度。如果沒有錯誤發生,bind()返回0。否則返回SOCKET_ERROR。
建立套接字連接──connect()與accept()
這兩個系統調用用于完成一個完整相關的建立,其中connect()用于建立連接。accept()用于使服務器等待來自某客戶進程的實際連接。
connect()的調用格式如下:
int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
參數s是欲建立連接的本地套接字描述符。參數name指出說明對方套接字地址結構的指針。對方套接字地址長度由namelen說明。
如果沒有錯誤發生,connect()返回0。否則返回值SOCKET_ERROR。在面向連接的協議中,該調用導致本地系統和外部系統之間連接實際建立。
由于地址族總被包含在套接字地址結構的前兩個字節中,并通過socket()調用與某個協議族相關。因此bind()和connect()無須協議作為參數。
accept()的調用格式如下:
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
參數s為本地套接字描述符,在用做accept()調用的參數前應該先調用過listen()。addr 指向客戶方套接字地址結構的指針,用來接收連接實體的地址。addr的確切格式由套接字創建時建立的地址族決定。addrlen 為客戶方套接字地址的長度(字節數)。如果沒有錯誤發生,accept()返回一個SOCKET類型的值,表示接收到的套接字的描述符。否則返回值INVALID_SOCKET。
accept()用于面向連接服務器。參數addr和addrlen存放客戶方的地址信息。調用前,參數addr 指向一個初始值為空的地址結構,而addrlen 的初始值為0;調用accept()后,服務器等待從編號為s的套接字上接受客戶連接請求,而連接請求是由客戶方的connect()調用發出的。當有連接請求到達時,accept()調用將請求連接隊列上的第一個客戶方套接字地址及長度放入addr 和addrlen,并創建一個與s有相同特性的新套接字號。新的套接字可用于處理服務器并發請求。
四個套接字系統調用,socket()、bind()、connect()、accept(),可以完成一個完全五元相關的建立。socket()指定五元組中的協議元,它的用法與是否為客戶或服務器、是否面向連接無關。bind()指定五元組中的本地二元,即本地主機地址和端口號,其用法與是否面向連接有關:在服務器方,無論是否面向連接,均要調用bind(),若采用面向連接,則可以不調用bind(),而通過connect()自動完成。若采用無連接,客戶方必須使用bind()以獲得一個唯一的地址。
監聽連接──listen()
此調用用于面向連接服務器,表明它愿意接收連接。listen()需在accept()之前調用,其調用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
參數s標識一個本地已建立、尚未連接的套接字號,服務器愿意從它上面接收請求。backlog表示請求連接隊列的最大長度,用于限制排隊請求的個數,目前允許的最大值為5。如果沒有錯誤發生,listen()返回0。否則它返回SOCKET_ERROR。
listen()在執行調用過程中可為沒有調用過bind()的套接字s完成所必須的連接,并建立長度為backlog的請求連接隊列。
調用listen()是服務器接收一個連接請求的四個步驟中的第三步。它在調用socket()分配一個流套接字,且調用bind()給s賦于一個名字之后調用,而且一定要在accept()之前調用。
數據傳輸──send()與recv()
當一個連接建立以后,就可以傳輸數據了。常用的系統調用有send()和recv()。
send()調用用于s指定的已連接的數據報或流套接字上發送輸出數據,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
參數s為已連接的本地套接字描述符。buf 指向存有發送數據的緩沖區的指針,其長度由len 指定。flags 指定傳輸控制方式,如是否發送帶外數據等。如果沒有錯誤發生,send()返回總共發送的字節數。否則它返回SOCKET_ERROR。
recv()調用用于s指定的已連接的數據報或流套接字上接收輸入數據,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
參數s 為已連接的套接字描述符。buf指向接收輸入數據緩沖區的指針,其長度由len 指定。flags 指定傳輸控制方式,如是否接收帶外數據等。如果沒有錯誤發生,recv()返回總共接收的字節數。如果連接被關閉,返回0。否則它返回SOCKET_ERROR。
輸入/輸出多路復用──select()
select()調用用來檢測一個或多個套接字的狀態。對每一個套接字來說,這個調用可以請求讀、寫或錯誤狀態方面的信息。請求給定狀態的套接字集合由一個fd_set結構指示。在返回時,此結構被更新,以反映那些滿足特定條件的套接字的子集,同時, select()調用返回滿足條件的套接字的數目,其調用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
參數nfds指明被檢查的套接字描述符的值域,此變量一般被忽略。
參數readfds指向要做讀檢測的套接字描述符集合的指針,調用者希望從中讀取數據。參數writefds 指向要做寫檢測的套接字描述符集合的指針。exceptfds指向要檢測是否出錯的套接字描述符集合的指針。timeout指向select()函數等待的最大時間,如果設為NULL則為阻塞操作。select()返回包含在fd_set結構中已準備好的套接字描述符的總數目,或者是發生錯誤則返回SOCKET_ERROR。
關閉套接字──closesocket()
closesocket()關閉套接字s,并釋放分配給該套接字的資源;如果s涉及一個打開的TCP連接,則該連接被釋放。closesocket()的調用格式如下:
BOOL PASCAL FAR closesocket(SOCKET s);
參數s待關閉的套接字描述符。如果沒有錯誤發生,closesocket()返回0。否則返回值SOCKET_ERROR。
以上就是socket編程的詳細講解的詳細內容,更多關于socket編程的資料請關注服務器之家其它相關文章!
原文鏈接:https://www.cnblogs.com/fengff/p/10984251.html