1、什么是socket,socket在哪?
Socket是應用層與 TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。socket起源於Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作,Socket就是該模式的一個實現,socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫、打開、關閉),這些函數我們在后面進行介紹。
有三種不同形式的套接字:流式套接字(SOCK_STREAM),數據包套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。
基於TCP的Socket使用流式套接字,相比於使用數據包套接字的UDP來講,TCP可以使程序員不必關心數據正確性及順序正確性,缺點是效率較低。
基於TCP的Socket編程最常見的應用場景是在C/S架構下的分布式應用,針對客戶端和服務器端提供不同的Socket系統調用。
2、client/server(CS)模式
服務端:服務器端: 初始化 socket套接字----->綁定socket----->對端口進行監聽(listen)----->阻塞(accept)----->等待客戶端連接,至此程序運行到剛啟動服務端的狀態。
客戶端:初始化 socket套接字------>發送連接請求(connect),如果連接成功,客戶端發送數據請求,服務器接受請求並處理請求,把回應數據發送給客戶端,客戶端讀取數據,最后關閉連接,完成一次交互。
3、實現代碼及相關API介紹
//服務器 #include<iostream> #include<winsock.h> #pragma comment(lib,"ws2_32.lib") using namespace std; void initialization(); int main() { //定義長度變量 int send_len = 0; int recv_len = 0; int len = 0; //定義發送緩沖區和接受緩沖區 char send_buf[100]; char recv_buf[100]; //定義服務端套接字,接受請求套接字 SOCKET s_server; SOCKET s_accept; //服務端地址客戶端地址 SOCKADDR_IN server_addr; SOCKADDR_IN accept_addr; initialization(); //填充服務端信息 server_addr.sin_family = AF_INET; server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(1234); //創建套接字 s_server = socket(AF_INET, SOCK_STREAM, 0); if (bind(s_server, (SOCKADDR*)& server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) { cout << "套接字綁定失敗!" << endl; WSACleanup(); } else { cout << "套接字綁定成功!" << endl; } //設置套接字為監聽狀態 if (listen(s_server, SOMAXCONN) < 0) { cout << "設置監聽狀態失敗!" << endl; WSACleanup(); } else { cout << "設置監聽狀態成功!" << endl; } cout << "服務端正在監聽連接,請稍候...." << endl; //接受連接請求 len = sizeof(SOCKADDR); s_accept = accept(s_server, (SOCKADDR*)& accept_addr, &len); if (s_accept == SOCKET_ERROR) { cout << "連接失敗!" << endl; WSACleanup(); return 0; } cout << "連接建立,准備接受數據" << endl; //接收數據 while (1) { recv_len = recv(s_accept, recv_buf, 100, 0); if (recv_len < 0) { cout << "接受失敗!" << endl; break; } else { cout << "客戶端信息:" << recv_buf << endl; } cout << "請輸入回復信息:"; cin >> send_buf; send_len = send(s_accept, send_buf, 100, 0); if (send_len < 0) { cout << "發送失敗!" << endl; break; } } //關閉套接字 closesocket(s_server); closesocket(s_accept); //釋放DLL資源 WSACleanup(); return 0; } void initialization() { //初始化套接字庫 WORD w_req = MAKEWORD(2, 2);//版本號 WSADATA wsadata; int err; err = WSAStartup(w_req, &wsadata); if (err != 0) { cout << "初始化套接字庫失敗!" << endl; } else { cout << "初始化套接字庫成功!" << endl; } //檢測版本號 if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) { cout << "套接字庫版本號不符!" << endl; WSACleanup(); } else { cout << "套接字庫版本正確!" << endl; } //填充服務端地址信息 }
//客戶端 #include<iostream> #include<winsock.h> #pragma comment(lib,"ws2_32.lib") using namespace std; void initialization(); int main() { //定義長度變量 int send_len = 0; int recv_len = 0; //定義發送緩沖區和接受緩沖區 char send_buf[100]; char recv_buf[100]; //定義服務端套接字,接受請求套接字 SOCKET s_server; //服務端地址客戶端地址 SOCKADDR_IN server_addr; initialization(); //填充服務端信息 server_addr.sin_family = AF_INET; server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); server_addr.sin_port = htons(1234); //創建套接字 s_server = socket(AF_INET, SOCK_STREAM, 0); if (connect(s_server, (SOCKADDR*)& server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) { cout << "服務器連接失敗!" << endl; WSACleanup(); } else { cout << "服務器連接成功!" << endl; } //發送,接收數據 while (1) { cout << "請輸入發送信息:"; cin >> send_buf; send_len = send(s_server, send_buf, 100, 0); if (send_len < 0) { cout << "發送失敗!" << endl; break; } recv_len = recv(s_server, recv_buf, 100, 0); if (recv_len < 0) { cout << "接受失敗!" << endl; break; } else { cout << "服務端信息:" << recv_buf << endl; } } //關閉套接字 closesocket(s_server); //釋放DLL資源 WSACleanup(); return 0; } void initialization() { //初始化套接字庫 WORD w_req = MAKEWORD(2, 2);//版本號 WSADATA wsadata; int err; err = WSAStartup(w_req, &wsadata); if (err != 0) { cout << "初始化套接字庫失敗!" << endl; } else { cout << "初始化套接字庫成功!" << endl; } //檢測版本號 if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) { cout << "套接字庫版本號不符!" << endl; WSACleanup(); } else { cout << "套接字庫版本正確!" << endl; } //填充服務端地址信息 }
linux下和c++中相關API介紹:
1)socket()函數
int socket(int domain, int type, int protocol);
domain:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET、AF_INET6,協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址與端口號的組合
type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM
protocol:指定協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP
在c++下,函數形式為
SOCKET PASCAL FAR socket(int af, int type, int protocol);
參數同上。
2)bind()函數
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:即socket描述字,它是通過socket()函數創建,唯一標識一個socket。bind()函數就是給這個描述字綁定一個名字
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址,這個地址結構根據地址創建socket時的地址協議族的不同而不同,如ipv4
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ };
addrlen:對應的是地址的長度
在c++下,函數形式為
int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
參數s是欲建立連接的本地套接字描述符,參數name指出說明對方套接字地址結構的指針,對方套接字地址長度由namelen說明。
如果沒有錯誤發生,connect()返回0,否則返回值SOCKET_ERROR。
3)listen()、connect()函數
int listen(int sockfd, int backlog); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
對於服務器來說,在調用socket()、bind()之后就會調用listen()來監聽這個socket,如果客戶端這時調用connect()發出連接請求,服務器端就會接收到這個請求。
listen()函數的第一個參數即為要監聽的socket描述字,第二個參數為相應socket隊列中允許的連接數目。socket()函數創建的socket默認是一個主動類型的,listen函數將socket變為被動類型的,等待客戶的連接請求。
connect()函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度,客戶端通過三次握手來建立與TCP服務器的連接。
在c++下,函數形式為
int PASCAL FAR listen(SOCKET s, int backlog); int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
listen()中,參數s標識一個本地已建立、尚未連接的套接字號,服務器願意從它上面接收請求。backlog表示請求連接隊列的最大長度,用於限制排隊請求的個數。如果沒有錯誤發生,listen()返回0。否則它返回SOCKET_ERROR。
connect()中,參數s是欲建立連接的本地套接字描述符,參數name指出說明對方套接字地址結構的指針,對方套接字地址長度由namelen說明。
4)accept()函數
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP服務器端依次調用socket()、bind()、listen()之后,就會監聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就向TCP服務器發送了一個連接請求。TCP服務器監聽到這個請求之后,就會調用accept()函數接收請求,這樣連接就建立好了,之后就可以開始I/O操作了。
accept()函數的第一個參數為服務器的socket描述字,第二個參數為指向struct sockaddr *的指針,第三個參數為協議地址的長度。如果accpet成功,那么其返回值是一個全新的描述字,返回客戶的TCP連接。
在c++中,函數形式為
SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
參數s為本地套接字描述符,在用accept()調用參數前應該先調用過listen()。addr 指向客戶方套接字地址結構的指針,用來接收連接實體的地址。addr的確切格式由套接字創建時建立的地址族決定。addrlen 為客戶方套接字地址的長度(字節數)。如果沒有錯誤發生,accept()返回一個SOCKET類型的值,表示接收到的套接字的描述符。否則返回INVALID_SOCKET。
5)send()、recv()函數
int send(int sockfd, const void *msg, int len, int flags); int recv(int sockfd, void *buf, int len, unsigned int flags);
send()中sockfd是你想發送數據的套接字描述字,msg 是指向你想發送的數據的指針,len是數據的長度,flags一般設置為0。
recv()中sockfd是要讀的套接字描述字,buf是要讀的信息的緩沖,len是緩沖的最大長度,flags一般設置為0。
在c++下,函數形式為
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags); int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
6)close()函數
int close(int fd);
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
在c++下,函數形式為
int PASCAL FAR closesocket ( IN SOCKET s);
4、程序運行結果: