Socket網絡編程(二):主要API調用方法

本文參考了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);
  1. 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。
  2. type 爲數據傳輸方式/套接字類型,常用的有 SOCK_STREAM(流格式套接字/面向連接的套接字) 和 SOCK_DGRAM(數據報套接字/無連接的套接字),我們已經在《套接字有哪些類型》一節中進行了介紹。
  3. 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() 不會。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章