socket技術詳解(看清socket編程)

https://blog.csdn.net/weixin_39634961/article/details/80236161程序員

socket編程是網絡經常使用的編程,咱們經過在網絡中建立socket關鍵字來實現網絡間的通訊,經過收集大量的資料,經過這一章節,充分的瞭解socket編程,文章用引用了大量大神的分析,加上本身的理解,作個總結性的文章編程

1:socket大體介紹

      socket編程是一門技術,它主要是在網絡通訊中常常用到服務器

      既然是一門技術,因爲如今是面向對象的編程,一些計算機行業的大神經過抽象的理念,在現實中經過反覆的理論或者實際的推導,提出了抽象的一些通訊協議,基於tcp/ip協議,提出大體的構想,一些泛型的程序大牛在這個協議的基礎上,將這些抽象化的理念接口化,針對協議提出的每一個理念,專門的編寫制定的接口,與其協議一一對應,造成了如今的socket標準規範,而後將其接口封裝成能夠調用的接口,供開發者使用網絡

     目前,開發者開發出了不少封裝的類來完善socket編程,都是更加方便的實現剛開始socket通訊的各個環節,因此咱們首先必須瞭解socket的通訊原理,只有從本質上理解socket的通訊,纔可能快速方便的理解socket的各個環節,才能從底層上真正的把握併發

2:TCP/IP協議

     要理解socket必須的得理解tcp/ip,它們之間比如送信的線路和驛站的做用,好比要建議送信驛站,必須得了解送信的各個細節。socket

     TCP/IP協議不一樣於iso的7個分層,它是根據這7個分層,將其從新劃分,比如打掃衛生,原本有掃帚,垃圾鬥,抹布,塗料,盆栽等就比如OSI的標準幾個分層,tcp/ip根據用途和功能,將掃帚,垃圾鬥放到粗略整理層,抹布塗料放到中度整理層,盆栽放到最終效果層。這裏TCP/IP也對OSI的網絡模型層進行了劃分:大體以下:tcp

OSI模型:ide


TCP/IP協議參考模型把全部的TCP/IP系列協議歸類到四個抽象層中函數

應用層:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等ui

傳輸層: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的一個例子,總結上述的問題

詳細就不在說明,經過一段代碼詳細的解釋

客戶端的代碼:

[cpp]  view plain  copy
  1. #include <winsock2.h>                
  2. #include <stdio.h>  
  3. #pragma comment(lib,"ws2_32.lib")  
  4. int main()  
  5. {  
  6.     //SOCKET前的一些檢查,檢查協議庫的版本,爲了不別的版本的socket,而且經過  
  7.     //WSAStartup啓動對應的版本,WSAStartup的參數一個是版本信息,一個是一些詳細的細節,注意高低位  
  8.     //WSAStartup與WSACleanup對應  
  9.     int err;  
  10.     WORD versionRequired;  
  11.     WSADATA wsaData;  
  12.     versionRequired=MAKEWORD(1,1);       
  13.     err=WSAStartup(versionRequired,&wsaData);//協議庫的版本信息  
  14.   
  15.     //經過WSACleanup的返回值來肯定socket協議是否啓動  
  16.     if (!err)  
  17.     {  
  18.         printf("客戶端嵌套字已經打開!\n");  
  19.     }  
  20.     else  
  21.     {  
  22.         printf("客戶端的嵌套字打開失敗!\n");  
  23.         return 0;//結束  
  24.     }  
  25.     //建立socket這個關鍵詞,這裏想一下那個圖形中的socket抽象層  
  26.     //注意socket這個函數,他三個參數定義了socket的所處的系統,socket的類型,以及一些其餘信息  
  27.     SOCKET clientSocket=socket(AF_INET,SOCK_STREAM,0);  
  28.   
  29.     //socket編程中,它定義了一個結構體SOCKADDR_IN來存計算機的一些信息,像socket的系統,  
  30.     //端口號,ip地址等信息,這裏存儲的是服務器端的計算機的信息  
  31.     SOCKADDR_IN clientsock_in;  
  32.     clientsock_in.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");  
  33.     clientsock_in.sin_family=AF_INET;  
  34.     clientsock_in.sin_port=htons(6000);  
  35.   
  36.     //前期定義了套接字,定義了服務器端的計算機的一些信息存儲在clientsock_in中,  
  37.     //準備工做完成後,而後開始將這個套接字連接到遠程的計算機  
  38.     //也就是第一次握手  
  39.   
  40.     connect(clientSocket,(SOCKADDR*)&clientsock_in,sizeof(SOCKADDR));//開始鏈接  
  41.   
  42.   
  43.     char receiveBuf[100];  
  44.   
  45.     //解釋socket裏面的內容  
  46.     recv(clientSocket,receiveBuf,101,0);  
  47.     printf("%s\n",receiveBuf);  
  48.   
  49.     //發送socket數據  
  50.     send(clientSocket,"hello,this is client",strlen("hello,this is client")+1,0);  
  51.   
  52.     //關閉套接字  
  53.     closesocket(clientSocket);  
  54.     //關閉服務  
  55.     WSACleanup();  
  56.     return 0;  
  57. }  


對應的服務端的代碼

[cpp]  view plain  copy
  1. #include <winsock2.h>  
  2. #include <stdio.h>  
  3. #pragma comment(lib,"ws2_32.lib")  
  4. int main()  
  5. {  
  6.        //建立套接字,socket前的一些檢查工做,包括服務的啓動  
  7.        WORD myVersionRequest;  
  8.        WSADATA wsaData;  
  9.        myVersionRequest=MAKEWORD(1,1);  
  10.        int err;  
  11.        err=WSAStartup(myVersionRequest,&wsaData);  
  12.        if (!err)  
  13.        {  
  14.               printf("已打開套接字\n");              
  15.        }  
  16.        else  
  17.        {  
  18.               //進一步綁定套接字  
  19.               printf("嵌套字未打開!");  
  20.               return 0;  
  21.        }  
  22.        SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//建立了可識別套接字  
  23.        //須要綁定的參數,主要是本地的socket的一些信息。  
  24.        SOCKADDR_IN addr;  
  25.        addr.sin_family=AF_INET;  
  26.        addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址  
  27.        addr.sin_port=htons(6000);//綁定端口  
  28.   
  29.        bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//綁定完成  
  30.        listen(serSocket,5);//其中第二個參數表明可以接收的最多的鏈接數  
  31.   
  32.        SOCKADDR_IN clientsocket;  
  33.        int len=sizeof(SOCKADDR);  
  34.        while (1)  
  35.        {  
  36.           //第二次握手,經過accept來接受對方的套接字的信息  
  37.               SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//若是這裏不是accept而是conection的話。。就會不斷的監聽  
  38.               char sendBuf[100];  
  39.               sprintf(sendBuf,"welcome %s to bejing",inet_ntoa(clientsocket.sin_addr));//找對對應的IP而且將這行字打印到那裏  
  40.               //發送信息  
  41.           send(serConn,sendBuf,strlen(sendBuf)+1,0);  
  42.               char receiveBuf[100];//接收  
  43.               recv(serConn,receiveBuf,strlen(receiveBuf)+1,0);  
  44.               printf("%s\n",receiveBuf);  
  45.               closesocket(serConn);//關閉  
  46.               WSACleanup();//釋放資源的操做  
  47.        }  
  48.        return 0;  
  49. }  



6:上面例子用到的知識點

 

(摘抄carter大神文章):

服務器端:

其過程是首先服務器方要先啓動,並根據請求提供相應服務:

(1)打開一通訊通道並告知本地主機,它願意在某一公認地址上的某端口(如FTP的端口可能爲21)接收客戶請求;

(2)等待客戶請求到達該端口;

(3)接收到客戶端的服務請求時,處理該請求併發送應答信號。接收到併發服務請求,要激活一新進程來處理這個客戶請求(如UNIX系統中用fork、exec)。新進程處理此客戶請求,並不須要對其它請求做出應答。服務完成後,關閉此新進程與客戶的通訊鏈路,並終止。

(4)返回第(2)步,等待另外一客戶請求。

(5)關閉服務器

客戶端:

(1)打開一通訊通道,並鏈接到服務器所在主機的特定端口;

(2)向服務器發服務請求報文,等待並接收應答;繼續提出請求......

(3)請求結束後關閉通訊通道並終止。

 

從上面所描述過程可知:

(1)客戶與服務器進程的做用是非對稱的,所以代碼不一樣。

(2)服務器進程通常是先啓動的。只要系統運行,該服務進程一直存在,直到正常或強迫終止。


 

7:下面就介紹一些API函數:(摘抄carter大神文章):

 

建立套接字──socket()

應用程序在使用套接字前,首先必須擁有一個套接字,系統調用socket()嚮應用程序提供建立套接字的手段,其調用格式以下:

[cpp]  view plain  copy
  1. 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()將套接字地址(包括本地主機地址和本地端口地址)與所建立的套接字號聯繫起來,即將名字賦予套接字,以指定本地半相關。其調用格式以下:

[cpp]  view plain  copy
  1. 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()的調用格式以下:

[cpp]  view plain  copy
  1. int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);  

參數s是欲創建鏈接的本地套接字描述符。參數name指出說明對方套接字地址結構的指針。對方套接字地址長度由namelen說明。

若是沒有錯誤發生,connect()返回0。不然返回值SOCKET_ERROR。在面向鏈接的協議中,該調用致使本地系統和外部系統之間鏈接實際創建。

因爲地址族總被包含在套接字地址結構的前兩個字節中,並經過socket()調用與某個協議族相關。所以bind()和connect()無須協議做爲參數。

accept()的調用格式以下:

[cpp]  view plain  copy
  1. 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()以前調用,其調用格式以下:

[cpp]  view plain  copy
  1. 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指定的已鏈接的數據報或流套接字上發送輸出數據,格式以下:

[cpp]  view plain  copy
  1. int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);  

參數s爲已鏈接的本地套接字描述符。buf 指向存有發送數據的緩衝區的指針,其長度由len 指定。flags 指定傳輸控制方式,如是否發送帶外數據等。若是沒有錯誤發生,send()返回總共發送的字節數。不然它返回SOCKET_ERROR。

recv()調用用於s指定的已鏈接的數據報或流套接字上接收輸入數據,格式以下:

[cpp]  view plain  copy
  1. 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()調用返回知足條件的套接字的數目,其調用格式以下:

[cpp]  view plain  copy
  1. 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()的調用格式以下:

[cpp]  view plain  copy
  1. BOOL PASCAL FAR closesocket(SOCKET s);  

參數s待關閉的套接字描述符。若是沒有錯誤發生,closesocket()返回0。不然返回值SOCKET_ERROR。

相關文章
相關標籤/搜索