目錄
- 流程概述
- 服務器端代碼實現
- 客戶端代碼實現
- 函數和結構講解
- sockaddr_in和sockaddr
- socket : 創建一個socket連接
- bind :綁定地址以及端口號問題
流程概述
客戶端與服務器之間的網絡通信基本原理如下所示,復雜一點的架構可能會添加消息中間件。
對于服務端,通信流程如下:
1、調用socket函數創建監聽socket
2、調用bind函數將socket綁定到某個IP和端口號組成的二元組上
3、調用listen函數開啟監聽
4、當有客戶端連接請求時,調用accept函數接受連接,產生一個新的socket(與客戶端通信的socket)
5、基于新產生的socket調用send或recv函數開始與客戶端進行數據交流
6、通信結束后,調用close函數關閉socket
對于客戶端,通信流程如下:
1、調用socket函數創建客戶端socket
2、調用connect函數嘗試連接服務器
3、連接成功后調用send或recv函數與服務器進行數據交流
4、通信結束后,調用close函數關閉監聽socket
服務器端代碼實現
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>using namespace std;
int main() {// 創建一個監聽socketint listenfd = socket(AF_INET, SOCK_STREAM, 0);if (listenfd == -1) {cout << " create listen socket error " << endl;return -1;}// 初始化服務器地址struct sockaddr_in bindaddr;bindaddr.sin_family = AF_INET;bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);bindaddr.sin_port = htons(3000);if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {cout << "bind listen socket error" << endl;return -1;}// 啟動監聽if (listen(listenfd, SOMAXCONN) == -1) {cout << "listen error" << endl;return -1;}while (true) {// 創建一個臨時的客戶端socketstruct sockaddr_in clientaddr;socklen_t clientaddrlen = sizeof(clientaddr);// 接受客戶端連接int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);if (clientfd != -1) {char recvBuf[32] = {0};// 從客戶端接受數據int ret = recv(clientfd, recvBuf, 32, 0);if (ret > 0) {cout << "recv data from cilent , data:" << recvBuf << endl;// 將接收到的數據原封不動地發給客戶端ret = send(clientfd, recvBuf, strlen(recvBuf), 0);if (ret != strlen(recvBuf)) {cout << "send data error" << endl;} else {cout << "send data to client successfully, data " << recvBuf <<endl;}} else {cout << "recv data error" <<endl;}close(clientfd);}}// 關閉監聽socketclose(listenfd);return 0;
}
客戶端代碼實現
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"using namespace std;int main() {// 創建一個socketint clientfd = socket(AF_INET, SOCK_STREAM, 0);if (clientfd == -1) {cout << " create client socket error " << endl;return -1;}// 連接服務器struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);serveraddr.sin_port = htons(SERVER_PORT);if (connect(clientfd, (struct sockaddr *)& serveraddr, sizeof(serveraddr)) == -1) {cout << "connect socket error" << endl;return -1;}// 向服務器發送數據int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);if (ret != strlen(SEND_DATA)) {cout << "send data error" << endl;return -1;} else {cout << "send data to client successfully, data " << SEND_DATA <<endl;}// 從服務器拉取數據char recvBuf[32] = {0};ret = recv(clientfd, recvBuf, 32, 0);if (ret > 0) {cout << "recv data to client successfully, data " << recvBuf <<endl;} else {cout << "recv data to client error" << endl;}// 關閉socketclose(clientfd);return 0;
}
函數和結構講解
sockaddr_in和sockaddr
在講解套接字編程函數之前,有必要對socket編程的兩個不可或缺的結構體進行說明:sockaddr
和 sockaddr_in
結構如下:
struct sockaddr{__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */char sa_data[14]; /* Address data. */};/* Structure describing an Internet socket address. */
struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. */struct in_addr sin_addr; /* Internet address. *//* Pad to size of `struct sockaddr'. */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];};
由于歷史的原因,套接字函數中(如connect,bind等)使用的參數類型大多是sockaddr類型的。而如今進行套接字編程的時候大都使用sockaddr_in進行套接字地址填充.因此,這就要求對這些函數進行調用的時候都必須要講套接字地址結構指針進行類型強制轉換,例如:
struct sockaddr_in serv;bind(sockfd,(struct sockaddr *)&serv,sizeof(serv));
socket : 創建一個socket連接
/* Create a new socket of type TYPE in domain DOMAIN, usingprotocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;
向用戶提供一個套接字,即套接口描述文件字,它是一個整數,如同文件描述符一樣,是內核標識一個IO結構的索引。
一般傳入參數是這樣的:
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
__domain:這個參數指定一個協議簇,也往往被稱為協議域。系統存在許多可以的協議簇,常見有AF_INET──指定為IPv4協議,AF_INET6──指定為IPv6,AF_LOCAL──指定為UNIX 協議域。這里指網絡層的協議
__type:這個參數指定一個套接口的類型,套接口可能的類型有:SOCK_STREAM
、SOCK_DGRAM
、SOCK_SEQPACKET
、SOCK_RAW
等等,它們分別表明字節流、數據報、有序分組、原始套接口。
__protocol:指定相應的傳輸協議,也就是諸如TCP或UDP協議等等,系統針對每一個協議簇與類型提供了一個默認的協議,我們通過把protocol設置為0來使用這個默認的值。這里指傳輸層的協議
返回值:socket函數返回一個套接字,即套接口描述字。如果出現錯誤,它返回-1,并設置errno為相應的值,用戶應該檢測以判斷出現什么錯
返回值:成功則返回0,失敗返回非0
bind :綁定地址以及端口號問題
在服務器端我們有這樣一段代碼:
// 初始化服務器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {cout << "bind listen socket error" << endl;return -1;
}
函數參數解釋:
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
bind的地址我們使用了宏INADDR_ANY
,這個宏定義如下:
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
如果應用程序不關心bind綁定的IP,則可以使用這個宏,底層的協議棧服務會自動選擇一個合適的IP地址,這樣在多個網卡機器上選擇IP地址會變得簡單。
如果只想在本機上進行訪問,bind函數地址可以使用本地回環地址
如果只想被局域網的內部機器訪問,那么bind函數地址可以使用局域網地址
如果希望被公網訪問,那么bind函數地址可以使用INADDR_ANY or 0.0.0.0
網絡通信的基本邏輯是客戶端連接服務器,即從客戶端的地址:端口 連接到 服務器的地址:端口上。
一般來說,服務器的端口號是固定的,而客戶端的端口號是連接發起時由操作系統隨機分配的,并且不會分配被占用的端口。端口號是一個short類型的值,其范圍為0~65535.
如果將bind函數中的端口號設置為0,那么操作系統會隨機為程序分配一個可用的監聽端口。一般來說,服務程序不會這么做,因為服務程序是要對外服務的,必須讓客戶端知道確切的IP地址和端口號。
在特殊的應用中,我們也可以在客戶端程序以指定的端口號連接服務器,與普通的流程相比就是在創建socket與發起connect之間多了一次bind操作:
![]() | ![]() |
其他相關函數可以到往期文章中查看:
socket編程常見函數使用方法