本文參考了http://c.biancheng.net/view/2123.html
Socket主要API調用方法
windows下socket的API和linux下的API大致相同,只是在某些細節上有些細微的差別。
包含頭文件和初始化
Linux socket常用頭文件
<sys/socket.h> //與套接字相關的函數聲明和結構體定義,如socket()、bind()、connect()及struct sockaddr的定義等
<sys/types.h> //primitive system data types(包含很多類型重定義,如pid_t、int8_t等)
<netinet/in.h> //某些結構體聲明、宏定義,如struct sockaddr_in、PROTO_ICMP、INADDR_ANY等
<arpa/inet.h> //某些函數聲明,如inet_ntop()、inet_ntoa()等
<sys/ioctl.h> //I/O控制操作相關的函數聲明,如ioctl()
<stdlib.h> //某些結構體定義和宏定義,如EXIT_FAILURE、EXIT_SUCCESS等
<netdb.h> //某些結構體定義、宏定義和函數聲明,如struct hostent、struct servent、gethostbyname()、gethostbyaddr()、herror()等
windows 頭文件及初始化
WinSock(Windows Socket)編程依賴於系統提供的動態鏈接庫(DLL),有兩個版本:
- 較早的DLL是 wsock32.dll,大小爲 28KB,對應的頭文件爲 winsock1.h;
- 最新的DLL是 ws2_32.dll,大小爲 69KB,對應的頭文件爲 winsock2.h。
使用 DLL 之前必須把 DLL 加載到當前程序,你可以在編譯時加載,也可以在程序運行時加載,這裏使用#pragma命令,在編譯時加載;
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")
在函數主體中使用相關API之前需要進行初始化;
WSAData wsaData;
if(0 != WSAStartup(MAKEWORD(2,2),&wsaData))
printf("初始化失敗!%d\n",WSAGetLastError());
socket()
socket()是用來創建套接字的,Linux中socket函數原型爲:
int socket(int af, int type, int protocol);
- af 爲地址族(Address Family),也就是 IP 地址類型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的簡寫,INET是“Inetnet”的簡寫。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
大家需要記住127.0.0.1,它是一個特殊IP地址,表示本機地址,後面的教程會經常用到。
你也可以使用 PF 前綴,PF 是“Protocol Family”的簡寫,它和 AF 是一樣的。例如,PF_INET 等價於 AF_INET,PF_INET6 等價於 AF_INET6。- type 爲數據傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字/面向連接的套接字) 和 SOCK_DGRAM(數據報套接字/無連接的套接字),我們已經在《套接字有哪些類型》一節中進行了介紹。
- protocol 表示傳輸協議,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分別表示 TCP 傳輸協議和 UDP 傳輸協議。
linux系統提供的socket()函數返回一個int整型,這個整型也就是一個FD文件描述符;
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP協議
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP協議
上面兩種情況都只有一種協議滿足條件,可以將 protocol 的值設爲 0,系統會自動推演出應該使用什麼協議:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //創建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //創建UDP套接字
windows系統提供的socket函數參數和用法相同,只是返回值是windows系統定義的SOCKET類型的句柄。
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); //創建TCP套接字
bind()和connect()
bind()函數原型
bind()函數的功能是將一個套接字socket與特定的IP地址和端口綁定。
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int bind(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
sock 爲 socket 文件描述符,addr 爲 sockaddr 結構體變量的指針,addrlen 爲 addr 變量的大小,可由 sizeof() 計算得出,返回值爲一個int整型,可以根據返回值判斷是否綁定成功。
下面給出一個示例,如何將套接字綁定到一個IP地址和端口:
//創建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//創建sockaddr_in結構體變量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每個字節都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具體的IP地址
serv_addr.sin_port = htons(1234); //端口
//將套接字和IP、端口綁定
if(SOCKET_ERROR ==bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
printf("bind failed!%d\n",WSAGetLastError());
可以看到這裏定義了一個sockaddr_in結構體來保存協議族、IP地址以及端口號,我們可以看一下sockaddr_in結構體的定義:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址類型
uint16_t sin_port; //16位的端口號
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
至於爲什麼要使用 sockaddr_in 結構體,然後再強制轉換爲 sockaddr 類型,其實這是一個歷史遺留問題,後來的接口總是得考慮兼容之前的代碼,所以這樣的操作看似繁瑣,卻很有必要。其實可以認爲,sockaddr 是一種通用的結構體,可以用來保存多種類型的IP地址和端口號,而 sockaddr_in 是專門用來保存 IPv4 地址的結構體。
另外綁定IP地址和綁定端口的時候有時候會用到htonl() 函數和htons() 函數,這個涉及到網絡數據大小端的問題:
在使用具體的IP地址的使用,直接傳入字符串則不需大小端轉換,直接使用inet_addr(char*)函數來綁定,如果要使用INADDR_ANY(多網卡IP地址綁定)則需要htonl(INADDR_ANY)來轉換,或者要通過一個端口號來綁定套接字的端口號,那麼則需要htos(int)來轉換。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //本地所有網卡的IP地址
serv_addr.sin_port = htons(8888); //端口
connect()函數原型
connect函數用於客戶端,將一個流類型套接字(TCP協議套接字)與服務端建立連接。
connect函數各個參數的含義與bind()函數完全一樣
int connect(int sock, struct sockaddr *addr, socklen_t addrlen); //Linux
int connect(SOCKET sock, const struct sockaddr *addr, int addrlen); //Windows
connect()調用之後線程會進入阻塞狀態,直至成功連接服務端。
listen()和accept()
listen()函數原型
listen函數可以讓服務端的一個套接字進入被動監聽的狀態,這個套接字我們稱它爲監聽套接字。
int listen(int sock, int backlog); //Linux
int listen(SOCKET sock, int backlog); //Windows
sock 爲需要進入監聽狀態的套接字,backlog 爲請求隊列的最大長度。
所謂被動監聽,是指當沒有客戶端請求時,套接字處於“睡眠”狀態,只有當接收到客戶端請求時,套接字纔會被“喚醒”來響應請求。
當套接字正在處理客戶端請求時,如果有新的請求進來,套接字是沒法處理的,只能把它放進緩衝區,待當前請求處理完畢後,再從緩衝區中讀取出來處理。如果不斷有新的請求進來,它們就按照先後順序在緩衝區中排隊,直到緩衝區滿。這個緩衝區,就稱爲請求隊列(Request Queue)。
可以將backlog 的值設置爲 SOMAXCONN,就由系統來決定請求隊列長度,這個值一般較大,可能爲幾百或者上千。
需要注意的是,listen()函數只是讓套接字進入一種監聽的狀態,並沒有真正接受請求,接受請求需要調用accept()函數。
accept()函數原型
當監聽套接字處於監聽狀態時,調用accept()函數可以接受客戶端的請求。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //Linux
SOCKET accept(SOCKET sock, struct sockaddr *addr, int *addrlen); //Windows
它的參數與 listen() 和 connect() 是相同的:sock 爲服務器端套接字,addr 爲 sockaddr_in 結構體變量,addrlen 爲參數 addr 的長度,可由 sizeof() 求得。accept()返回一個套接字,這個返回的套接字纔是與客戶端建立連接的套接字,而不是之前的監聽套接字。
和connect類似,accept()也會阻塞線程,直至接受到請求。
send()/recv()和write()/read()
windows下發送、接受
int send(SOCKET sock, const char *buf, int len, int flags); //發送
int recv(SOCKET sock, char *buf, int len, int flags); //接受
sock 爲要發送數據的套接字,buf 爲要發送的數據的緩衝區地址,len 爲要發送的數據的字節數,flags 爲發送數據時的選項,一般將flags置爲0或者NULL。
linux下發送、接受
ssize_t write(int fd, const void *buf, size_t nbytes); //發送
ssize_t read(int fd, void *buf, size_t nbytes); //接受
fd 爲要寫入的文件的描述符,buf 爲要寫入的數據的緩衝區地址,nbytes 爲要寫入的數據的字節數。
Linux 不區分套接字文件和普通文件,使用 write() 可以向套接字中寫入數據,使用 read() 可以從套接字中讀取數據。
close()/closesocket()與shutdown()
調用 close()/closesocket() 函數意味着完全斷開連接,即不能發送數據也不能接收數據,這種“生硬”的方式有時候會顯得不太“優雅”。
shutdown()函數原型
int shutdown(int sock, int howto); //Linux
int shutdown(SOCKET s, int howto); //Windows
sock 爲需要斷開的套接字,howto 爲斷開方式。
howto 在 Linux 下有以下取值:
SHUT_RD:斷開輸入流。套接字無法接收數據(即使輸入緩衝區收到數據也被抹去),無法調用輸入相關函數。
SHUT_WR:斷開輸出流。套接字無法發送數據,但如果輸出緩衝區中還有未傳輸的數據,則將傳遞到目標主機。
SHUT_RDWR:同時斷開 I/O 流。相當於分兩次調用 shutdown(),其中一次以 SHUT_RD 爲參數,另一次以 SHUT_WR 爲參數。
howto 在 Windows 下有以下取值:
SD_RECEIVE:關閉接收操作,也就是斷開輸入流。
SD_SEND:關閉發送操作,也就是斷開輸出流。
SD_BOTH:同時關閉接收和發送操作。
確切地說,close() / closesocket() 用來關閉套接字,將套接字描述符(或句柄)從內存清除,之後再也不能使用該套接字,與C語言中的 fclose() 類似。應用程序關閉套接字後,與該套接字相關的連接和緩存也失去了意義,TCP協議會自動觸發關閉連接的操作。
shutdown() 用來關閉連接,而不是套接字,不管調用多少次 shutdown(),套接字依然存在,直到調用 close() / closesocket() 將套接字從內存清除。
調用 close()/closesocket() 關閉套接字時,或調用 shutdown() 關閉輸出流時,都會向對方發送 FIN 包。FIN 包表示數據傳輸完畢,計算機收到 FIN 包就知道不會再有數據傳送過來了。
默認情況下,close()/closesocket() 會立即向網絡中發送FIN包,不管輸出緩衝區中是否還有數據,而shutdown() 會等輸出緩衝區中的數據傳輸完畢再發送FIN包。也就意味着,調用 close()/closesocket() 將丟失輸出緩衝區中的數據,而調用 shutdown() 不會。