- Socket是針對端系統,也就是用戶主機上開發程序,不涉及網絡設備(交換機、路由器)
- 獨立于網卡驅動層之上,不涉及硬件,即基于Packet Driver編程
- 端:是指通信雙方兩臺電腦
- ?應用編程接口API 也就是兩端 應用層內部的應用進程之間的? 數據通信,遵循應用層協議,他們之間數據通信需要底層(傳輸層、網絡層、數據鏈路層、物理層)的支持,底層一般是涉及到操作系統的知識
- 應用進程 和 操作系統 之間需要一個接口,這個接口就是應用編程接口 API,這個接口API就是應用進程的控制權和操作系統的控制權之間進行轉換的一個系統調用的接口。即應用進程通過API接口將控制權交給操作系統,操作系統執行完成之后將執行結果返還給我們的應用進程
- ?幾種典型的應用編程接口
- UNIX環境下? 套接字接口? 簡稱套接字 socket
- 微軟的 Windows Socket Interface 即 WINSOCK
- Socket API
- 適用于絕大多數操作系統
- 實現了應用進程間通信的抽象機制
- 面向多種協議棧接口 TCP /IP
- Internet網絡應用最為典型的API接口
- 通信模型:客戶/服務器 (C/S)架構
?問題:主機和客戶端都運行了多個應用進程,如何保證客戶端進程和與之對應的服務器應用進程正確匹配呢?
- IP地址只能區分 主機
- 考慮到 跨主機進程通信需要傳輸層的支持,使用端口號區分 不同進程之間的標識
- ?服務器對外提供服務 需要提供 IP地址 + 端口號
- 但是服務器內部使用套接字描述符來管理套接字,這個描述符本質就是一個結構體指針,結構體內部詳細記錄了字段對應的信息
- Socket抽象,其操作類型對于文件的操作,將其看做一個特殊的文件
- 返回的套接字描述符
- 最為關鍵的是地址信息
- 短點地址 = IP地址 + 端口號
- ?使用套接字的時候需要指定本地和遠程的IP地址和端口號 ,就需要使用sockaddr_in設置端點地址,就包含了IP地址和端口號等信息
- 地址族:一般使用AF_INET? 涉及到不同的協議棧
Socket?API函數 (WinSock)
- 基于linux的socket進行擴充
- 前面加入了WSA 表示這個版本的 套接字工具 采用動態鏈接庫的方式進行創建
WSAStartup
?WSACleanup
- 不帶WSA的函數接口適用于 WIN或者Linux,帶上WSA的函數只能適用于win環境下
- int WSACleanup(void);
- 應用程序在完成對請求的Socket庫的使用,最后需要調用WSACleanup函數,從而解除和Socket庫的綁定,釋放Socket庫所占用的系統資源
socket
- 創建套接字,銜接 應用層和傳輸層之間的數據通信
- ?套接字的類型
- 使用的是TCP類型? ?套接字的類型是SOCK_STREAM
- 使用的是UDP類型? 套接字的類型是 SOCK_DGRAM
- 跳過傳輸層,直接實現 應用層和網絡層之間的數據通信,使用SOCK_RAW,需要特殊的權限,linux操作的話需要具備root權限,win需要用戶具備管理員權限,其具備 上述兩種方式的獨特之處?
- TCP 和 UDP的區別
- TCP:可靠 (數據不會出錯、丟失、亂序等)、面向連接、字節流傳輸、點對點
- UDP:不可靠、無連接、數據報傳輸
closesocket
- win使用的是closesocket
- linux使用的是close
- int closesocket(SOCKET sd);
- 關閉一個描述符為sd的套接字
- 如果多個進程共享一個套接字的話,調用closesocket將套接字的引用計數減一,減少至0才會真正關閉
- 一個進程中的多個線程對一個套接字的使用無計數
- 即同一個進程中的一個線程使用closesocket關閉套接字,這個進程中的其余線程也不能訪問這個套接字
- 返回數值
- 0? : 成功
- SOCKET_ERROR? :? 失敗
bind
- socket創建的時候,內部的套接字描述符并沒有涵蓋地址信息,比如IP地址和端口號,需要使用bind進行綁定,設定套接字的本地斷點地址?
- 參數
- 套接字描述符:也就是使用socket創建的
- 端點地址
- 結構 sockaddr_in
- 客戶程序不需要調用bind函數,因為操作系統會幫助用戶填充IP地址和端口號
- 服務器需要使用這個函數指定IP地址和端口號
- IP地址如何綁定呢?
- 如果主機安置了不同的網卡,分別連接在不同的網段,造成了網段隔離
- 所以需要使用地址通配符? INADDR_ANY
- IP地址如何綁定呢?
listen
- int listen(sd,queuesize);
- 置服務器端的流套接字處于被動監聽狀態
- 僅僅服務端調用
- 僅用于面向連接的流套接字
- 設置了連接請求隊列的大小
- 返回數值
- 0 成功
- SOCKET_ERROR:失敗
connect
- connect (sd,saddr,saddrlen)
- 客戶端套接字? sd
- 特定計算機的特定端口? saddr
- 只適用于客戶端,使用connect函數和服務器進行連接
- 通信協議
- TCP:建立TCP連接
- connect返回成功,表示建立連接,可以成功通信
- UDP:指定服務器的端點地址
- connect返回成功,可不一定會成功通信
- TCP:建立TCP連接
accept
- newsock = accept(sd,caddr,caddrlen);
- 服務端調用accept函數從處于監聽狀態的流套接字sd中的客戶連接請求隊列中取出排在最前面的一個客戶請求,并且創建一個新的套接字 銜接 來自客戶端的套接字,形成一個通道
- 注意事項:
- 僅用于TCP套接字
- 僅用于服務器?
- 創建新的套接字,使用新的套接字 和 客戶端 進行通信
- 原因
- TCP是面向連接的、可靠的、點對點的。
- 也就是客戶端和服務端 通過一個Socket建立連接,然后創建新的socket負責 客戶端和服務端的應用進程之間的通信,出于高并發的思想采用上述設計
- 要不 服務端只能為一個客戶端提供服務,就不能同時為其余客戶端的用戶進行服務
- 主線程或主進程繼續監聽新的請求,子線程通過創建新的連接請求
- 原因
send / send to?
- send (sd,*buf,len,flags)
- sendto(sd,*buf,len,flags,destaddr,addrlen)
- send函數TCP套接字(客戶和服務器) 或調用了connnect函數的UDP客戶端套接字,適用于TCP,因為已經連接了,就不需要ip地址和端口號了
- sendto函數適用于UDP服務器套接字 與? 沒有調用connect函數的UDP客戶端套接字
recv / recv from
- recv(sd,*buffer,len,flags)
- rtecvfrom(sd,*buf,len,flags,senderaddr,saddrlen)
- recv 函數從TCP連接的另外一端接收數據,或者從調用了connect函數的UDP客戶端套接字接收服務器發來的數據
- recvfrom 函數從UDP服務器套接字與未調用connnect函數的UDP的客戶端套接字接收對端數據
setsockopt 和 getsockopt? 使用不多?
小結
- connect 如果是tcp的話是真正的連接,如果是UDP沒有連接,僅僅指定一個端口和地址
?網絡字節順序
- 表示層 解決數據表示轉換功能
- TCP/IP 定義了標準的用于協議頭中的二進制的整數表示:網絡字節順序
- 某些Socket API函數的參數需要存儲為網絡字節順序 (IP地址和端口號等等)
- 可以實現本地字節順序和網絡字節順序的轉換
- htons:? ?將本地字節順序? 轉換為? 網絡字節順序 (16bits)
- ntohs:? ?將網絡字節順序? 轉換為?本地字節順序 (16bits)
- htonl :? ?將本地字節順序? 轉換為? 網絡字節順序 (32bits)
- ntohl:? ? 將網絡字節順序? 轉換為?本地字節順序 (16bits)
解析服務器的IP地址
- 客戶端使用域名、IP地址標識服務器,但是IP協議需要的是32位二進制的IP地址,因此需要將域名或IP地址轉換為32位IP地址
- 函數
- inet_addr() 實現點分十進制IP地址到32位IP地址的轉換
- gethostbyname()實現域名到32位IP地址的轉換
- 返回一個指向hostent結構體的指針?
解析服務器(熟知)端口號
- 客戶端還可以使用 服務名 標識服務器端口
- 需要將服務器名 轉換為 熟知的端口號
- 函數
- getservbyname()
- 返回一個指向servent結構的指針?
- getservbyname()
?解析協議號
- 客戶端可能使用協議名 如 TCP 指定協議
- 需要將協議名轉換為協議號 如 6
- 函數
- getprotobyname()實現協議名到協議號的轉換
- 返回一個指向結構protoent的指針
?TCP客戶端軟件
- 確定服務器的IP地址和端口號
- 創建套接字
- 分配本地端點地址(IP地址和端口號)
- 連接服務器(套接字)
- 遵循應用層協議進行通信
- 關閉釋放連接
UDP客戶端軟件
- 確定服務器的IP地址和端口號
- 創建套接字
- 分配本地端點地址(IP地址和端口號)
- 指定服務器端點地址 構造UDP數據包
- 遵循應用層協議進行通信
- 關閉釋放連接
客戶端軟件的實現 connectsock()
- 設計一個connectsock過程封裝底層的代碼
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<winsock.h>#ifdef INADDR_NONE
#define INADDR_NONE 0xffffffff
#endif /*INADDR_NONE*/void errexit(const char *);
/*connectsock --> allocate & connect a socket using TCP or UDP
*///host 服務器端點地址
//service 服務名
//transport 傳輸層協議
SOCKET connectsock(const char * host,const char* service,const char * transport){struct hostent *phe; //pointer to host information entry struct servent *pse; //pointer to service information entrystruct protoent *ppe; //pointer to protocal iunformation entrystruct sockaddr_in sin; //an Internet endpoint addressint s,type; //socket descriptor and socket typememset(&sin,0,sizeof(sin));sin.sin_family = AF_INET;//Map service name to port numberif(pse = getservbyname(service,transport)){sin.sin_port = pse.s_port;}else if((sin.sin_port = htons((u_short)atoi(service))) == 0){errexit("can't get \"%s\" service entry\n",service);}//Map host name to IP address,allowing for dotted decimal_pointif(phe = gethostbyname(host)){memcpy(&cin.sin_addr,phe->h_addr,phe->h_length);}else if((sin.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE){errexit("can't get \"%s\" host entry\n",host);}//Map protocol name to protocal numberif(ppe = getprotobyname(transport) == 0){errexit("can't get \"%s\" protocal entry\n",transport);}//Use protocal to choose a socket typeif(strcmp(transport,"udp")==0){type = SOCK_DGRAM;}else{type = SOCK_STREAM;}//Allocate a sockets = socket(PF_INET,type,ppe->p_proto);if(s == INVALID_SOCKET){errexit("can't create socket:%d",GetLastError());}//Connect the socketif(connect(s,(struct sockaddr*)&sin,sizeof(sin)) == SOCKET_ERROR){errexit("can't connect to %s.%s: %d\n",host,service,GetLastError());}return s;
}
- UDP客戶端 和 TCP客戶端
#include <winsock.h>
SOCKET connectsock(const char*,const char*,const char*);//connectUDP -> connect to a specified UDP service on a specified host
SOCKET connectUDP(const char*host,const char* service){return connectsock(host,service,"udp")
}#include <winsock.h>
SOCKET connectsock(const char*,const char*,const char*);//connectUDP -> connect to a specified UDP service on a specified host
SOCKET connectTCP(const char*host,const char* service){return connectsock(host,service,"tcp")
}
- 異常處理
#include<stdarg.h>
#include<stdio.h>
#include<stdlib.h>
#include<winsock.h>//errexit -> print an error message and exitvoid errexit(const char* format){va_list args;va_start(args,format);vfprintf(stderr,format,args);va_end(args);WSACleanup();exit(1);
}
訪問DAYTIME服務的客戶端
- 獲取日期和時間
- 雙協議服務(TCP\UDP)
- 端口號為 13
- localhost 說明 服務端和客戶端都部署在同一臺機器上
- daytime 說明這是一個請求時間的服務
TCP
- 代碼采用循環接收信息的方式來接收,是因為tcp是一種流傳輸的協議,發送端發送的數據并不意味著接收端收到的數據一樣?,有可能數據切片
?UDP
?
?UDP使用數據報的方式,因此每次發送和接收的數據是完整的,因此不需要使用循環,只需要一次接收即可
?四種類型基本服務器
-
循環無連接? UDP
- 流程
- 創建套接字
- 綁定端點地址 INADDR_ANY + 端口號
- 反復接收來自客戶端的請求
- 遵循應用層協議,構造響應報文,發送給客戶
- 數據發送
- 服務器不可以使用connect()函數
- 無連接服務器使用sendto()函數發送數據報
- 數據接收
- 使用recvfrom函數接收數據,自動提取
- 流程
-
循環面向連接? TCP
- 創建(主)套接字? 并綁定端口號
- 設置(主)套接字為被動監聽模式,準備用于服務器
- 調用accept()函數接收下一個請求(通過主套接字),創建新的套接字用于和客戶端建立連接
- 遵循應用層協議,反復接收客戶請求,通過新的套接字構造并發送響應報文,發送給客戶
- 完成為特定的客戶端服務之后,關閉和客戶端之間的連接 返回步驟3
-
并發無連接
- 主線程 第一步?創建套接字? 并綁定端口號
- 主線程 第二步? 反復調用recvfrom()函數 接收下一個客戶端的請求,并創建新線程 處理客戶響應
- 子線程 第一步 接受特定的請求
- 子線程 第二步 依據應用層的協議構造響應的報文? 并調用sendto()發送
- 子線程 第三步 退出(子線程處理完成一個請求之后便會終止)
- 注意事項:
- 主線程仍然在調用 recvfrom函數,不斷的接收請求 創建線程響應服務
- 并發面向連接
- 主線程 第一步?創建(主)套接字? 并綁定端口號
- 主線程 第二步?設置(主)套接字為被動監聽模式,準備用于服務器。
- 主線程 第三部?反復調用accept()函數 創建主套接字,接收下一個客戶端的下一個連接請求,并創建新線程 處理客戶響應
- 子線程 第一步 接受特定的請求
- 子線程 第二步 依據應用層的協議與特定的用戶進行交互
- 子線程 第三步 退出(子線程處理完成一個請求之后便會終止)(線程終止)
代碼
?