版權(quán)聲明:本文為 cdeveloper 原創(chuàng)文章锐膜,可以隨意轉(zhuǎn)載,但必須在明確位置注明出處腮鞍!
Socket 接口簡介
Socket 套接字是由 BSD(加州大學(xué)伯克利分校軟件研發(fā)中心)開發(fā)的一套獨(dú)立于具體協(xié)議的網(wǎng)絡(luò)編程接口值骇,應(yīng)用程序可以用這個(gè)接口進(jìn)行網(wǎng)絡(luò)通信。要注意:Socket 不是一套通信協(xié)議(HTTP缕减,F(xiàn)TP 等是通信協(xié)議)雷客,而是編程的接口,即我們在程序中使用的網(wǎng)絡(luò)函數(shù)桥狡。TCP/IP 網(wǎng)絡(luò)編程底層就是使用 Socket 接口來通信搅裙,所以在學(xué)習(xí) TCP/IP 編程之前必須知道 Socket 的接口使用方法。
Socket API 接口
基本的編程接口有:socket裹芝,bind部逮,listen,accept嫂易,connect兄朋,send,recv 等怜械,這些函數(shù)都很重要颅和,下面來一一學(xué)習(xí)這些函數(shù)傅事。
創(chuàng)建通信套接字:socket
socket 函數(shù)創(chuàng)建一個(gè)通信的端點(diǎn),并返回一個(gè)指向該端點(diǎn)的文件描述符(Linux 下一切皆是文件):
#include <sys/types.h>
#include <sys/socket.h>
/*
* domain: 通信協(xié)議簇峡扩,例如 AF_INET, AF_UNIX...
* type: SOCK_STREAM, SOCK_DGRAM 等等
* protocol: 通常為 0
* return: 成功返回文件描述符蹭越,失敗返回 -1,并設(shè)置 erron
*/
int socket(int domain, int type, int protocol);
例如服務(wù)器端創(chuàng)建一個(gè)用于接受客戶端連接的 socket 的代碼:
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == server_fd) {
perror("socket");
exit(1);
}
分配套接字名稱:bind
當(dāng)用 socket 函數(shù)創(chuàng)建套接字后教届,并沒有為它分配 IP 地址和端口响鹃,我們還需要使用 bind 函數(shù)來將指定的 IP 和端口分配給已經(jīng)創(chuàng)建的 socket:
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd: socket 返回的文件描述符
* addr: 含有要綁定的 IP 和端口的地址結(jié)構(gòu)指針
* addrlen: 第二個(gè)參數(shù)的大小,使用 sizeof 來計(jì)算
* return: 成功返回 0案训,失敗返回 -1买置,并設(shè)置 erron
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
其實(shí)第二個(gè)參數(shù)要注意,參數(shù)指定的是 struct sockaddr *
類型强霎,一般不直接使用這個(gè)結(jié)構(gòu)忿项,這個(gè)類型在 Linux 上有許多的變種,例如 sockaddr_in 和 sockaddr_un脆栋,經(jīng)常使用后面 2 個(gè)結(jié)構(gòu)定義 IP 和端口信息倦卖,在 bind 是強(qiáng)制轉(zhuǎn)換成 struct sockaddr *
類型:
// struct sockaddr_un myaddr;
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
// 接受任何 IP 地址的連接
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 指定連接端口為 8080
myaddr.sin_port = htons(8080);
if(bind(server_fd, (struct sockaddr *)&myaddr, sizeof(sockaddr_in)) == -1) {
perror("bind");
exit(1);
}
開始監(jiān)聽:listen
使用 listen 來建立一個(gè)監(jiān)聽客戶端連接的隊(duì)列:
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd: 監(jiān)聽的 socket 描述符
* backlog: 建立的最大連接數(shù)
* return: 成功返回 0洒擦,失敗返回 -1椿争,并設(shè)置 erron
*/
int listen(int sockfd, int backlog);
例如創(chuàng)建一個(gè)可以監(jiān)聽 10 個(gè)客戶端連接請求的隊(duì)列:
if(listen(server_fd, 10) == -1) {
perror("listen");
exit(1);
}
接受連接請求:accept
網(wǎng)絡(luò)編程的核心一步就是建立客戶端和服務(wù)器端的連接,使用 accept 來建立 2 者的連接:
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd: 已經(jīng)創(chuàng)建的本地正在監(jiān)聽的 socket
* addr: 保存連接的客戶端的地址信息
* addrlen: sockaddr 的長度指針
* return: 成功返回客戶端的 socket 文件描述符號熟嫩,失敗返回 -1秦踪,設(shè)置 erron
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
后兩個(gè)參數(shù)我們需要定義,但是不需要初始化掸茅,在連接成功后客戶端的 socket 信息會(huì)自動(dòng)地填入第 3 個(gè)結(jié)構(gòu)中椅邓。使用方法如下:
struct sockaddr_in clientaddr;
int clientaddr_len = sizeof(clientaddr);
// 建立連接請求
int client_fd = accept(server_fd, (struct sockaddr *)&clientaddr, &clientaddr_len);
if(client_fd == -1) {
perror("accept error") ;
exit(1) ;
}
// socket fd 使用完畢也必須關(guān)閉
close(client_fd);
發(fā)送數(shù)據(jù):send,sendto
在建立連接之后昧狮,當(dāng)然要發(fā)送數(shù)據(jù)景馁,既然 socket 也是文件,發(fā)送數(shù)據(jù)其實(shí)也就是寫文件逗鸣,我們使用 send 函數(shù)來發(fā)送 socket 數(shù)據(jù):
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd: 接受數(shù)據(jù)的 socket
* buf: 發(fā)送的數(shù)據(jù)
* len: 數(shù)據(jù)長度
* flags: 當(dāng)這個(gè)參數(shù)為 0合住,該函數(shù)等價(jià)與 write
* return: 成功返回發(fā)送的字節(jié)數(shù),失敗返回 -1撒璧,并設(shè)置 erron
*/
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/* sendto 功能是將數(shù)據(jù)發(fā)送到指定的地址 dest_addr透葛,其他參數(shù)基本相同 */
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
例如服務(wù)器在建立連接后發(fā)送一個(gè)字符串到客戶端:
char msg[] = "Hello Client."
send(client_fd, msg, strlen(msg), 0);
sendto(client_fd, msg, strlen(msg), 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
接收數(shù)據(jù):recv,recvfrom
既然有發(fā)送數(shù)據(jù)卿樱,必然有接收數(shù)據(jù)的函數(shù)僚害,與 send 類似,recv 的功能也跟 read 幾乎相同:
#include <sys/types.h>
#include <sys/socket.h>
/*
* sockfd: 接收的 socket fd
* buf: 接收緩沖區(qū)
* len: 緩沖區(qū)長度
* flags: 當(dāng)這個(gè)參數(shù)為 0繁调,該函數(shù)等價(jià)與 read
* return: 成功返回接受的字節(jié)數(shù)萨蚕,失敗返回 -1靶草,并設(shè)置 erron
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/* recvfrom 從指定的地址 src_addr 接收數(shù)據(jù),其他參數(shù)與 recv 類似 */
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
例如接受服務(wù)器發(fā)送的字符串:
char msg_buf[100] = { 0 };
recv(server_fd, msg_buf, 100, 0);
int srcaddr_len = sizeof(src_addr);
recvfrom(server_fd, msg_buf, 100, 0,
(struct sockaddr*)&src_addr, &srcaddr_len);
基本的 socket 函數(shù)就介紹完了岳遥,但是這里不可能將所有細(xì)節(jié)都列出來爱致,想要深入的學(xué)習(xí)建議你查看對應(yīng)函數(shù)的 man 手冊,例如 man socket
寒随,man recvfrom
等等糠悯。
使用 Socket 進(jìn)行 TCP 通信
TCP 通信的概念在上一篇文章中已經(jīng)介紹過了,這里使用 Socket 提供的編程接口來實(shí)際編寫一個(gè)簡單的服務(wù)器和客戶端來模擬通信過程妻往,下面是使用 Socket 進(jìn)行 TCP 通信的過程:
1. TCP 服務(wù)器
其中用到的都是上面介紹的 socket 函數(shù)互艾,整個(gè)通信過程不算很復(fù)雜,主要是:建立連接-傳輸或處理數(shù)據(jù)-關(guān)閉連接讯泣。下面就是一個(gè)基于 TCP 的簡單服務(wù)器的例子纫普,這里為了防止代碼過多就省去了返回值檢查的過程(檢查過程可以參考前面的例子):
// tcp_server.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <arpa/inet.h>
int main(void) {
int server_fd, client_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
// 1. init server addr
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
int client_addr_len = sizeof(client_addr);
// 2. create socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 3. bind server_addr to server_fd
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
// 4. listen server_fd, max listen client num = 10
listen(server_fd, 10);
printf("TCP server is listening...\n");
// 5. accept client connect
char send_msg[] = "hello client";
while(1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
// 6. send data
send(client_fd, send_msg, sizeof(send_msg), 0);
printf("Write \"hello client\" to client ok.\n");
// 7. close client fd
close(client_fd);
}
// 8. close server fd
close(server_fd);
return 0;
}
這個(gè)例子非常基礎(chǔ)好渠,但是還是有 4 處容易出錯(cuò)的地方:
-
INADDR_ANY
: 允許任何 IP 地址的客戶端連接本服務(wù)器 - 指定端口為
8888
: 連接的端口被指定為 8888昨稼,如果連接不成功則端口可能被占用,可以嘗試更換端口 -
AF_INET
和SOCK_STREAM
: 兩者確定了當(dāng)前使用的是 TCP 協(xié)議 - 強(qiáng)制轉(zhuǎn)換為
struct sockaddr *
: 在使用 bind 和 accept 時(shí)拳锚,都需要將sockaddr_in
或sockaddr_un
強(qiáng)制轉(zhuǎn)換為這個(gè)類型 - 不要忘記關(guān)閉 socket 的文件描述符 fd
編譯運(yùn)行該服務(wù)器:
gcc tcp_server.c -o tcp_server
./tcp_server
TCP server is listening...
^C
運(yùn)行正常假栓,下面來看看 TCP 客戶端的代碼。
2. TCP 客戶端
TCP 客戶端直接創(chuàng)建 socket霍掺,然后使用 connect 連接服務(wù)器匾荆,之后接收服務(wù)器發(fā)送的數(shù)據(jù):
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: ./tcp_client localhost\n");
exit(1);
}
int server_fd = 0;
// 1. init client addr
struct sockaddr_in client_addr;
client_addr.sin_family = AF_INET;
struct hostent *myhost = gethostbyname(argv[1]);
client_addr.sin_addr = (*((struct in_addr *)(myhost->h_addr)));
client_addr.sin_port = htons(8888);
// 2. create socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 3. connect server
connect(server_fd, (struct sockaddr *)&client_addr, sizeof(client_addr));
// 4. recv msg from server
char msg_buf[100] = { 0 };
recv(server_fd, msg_buf, 100, 0);
printf("Client get server msg: %s\n", msg_buf);
// 5. close fd
close(server_fd);
return 0;
}
要注意的是客戶端這里根據(jù)實(shí)際的域名來獲取 IP,也可以使用下面的代碼直接使用指定的 IP 地址:
client_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
編譯看看:
gcc tcp_client.c -o tcp_client
./tcp_client localhost
沒有錯(cuò)誤杆烁,下面來測試下牙丽。
3. 測試 TCP 連接
先運(yùn)行服務(wù)器:
./tcp_server
TCP server is listening...
在新的終端中運(yùn)行客戶端:
./tcp_client localhost
Client get server msg: hello client
成功接收了服務(wù)器的消息,并且服務(wù)器也打印了消息:
./tcp_server
Write "hello client" to client ok.
這就成功的實(shí)現(xiàn)了一個(gè)簡單的使用 TCP 實(shí)現(xiàn)服務(wù)器端和客戶端通信的例子了兔魂,例子實(shí)現(xiàn)起來很簡單烤芦,把前面介紹的 API 理解并學(xué)會(huì)使用就可以了。下面再來看看使用 UDP 的如何實(shí)現(xiàn)兩個(gè)網(wǎng)絡(luò)進(jìn)程的通信析校。
使用 Socket 進(jìn)行 UDP 通信
UDP 通信的基本概念也在上一篇文章中构罗,可以點(diǎn)擊查看,UDP 是一種面向無連接的協(xié)議勺良,因而具有資源消耗小绰播,處理速度快的優(yōu)點(diǎn),所以通常音頻尚困、視頻等實(shí)時(shí)性較強(qiáng)的數(shù)據(jù)在傳送時(shí)使用 UDP 較多蠢箩,因?yàn)樗鼈兗词古紶杹G失一兩個(gè)數(shù)據(jù)包,也不會(huì)對接收結(jié)果產(chǎn)生太大影響,比如 QQ 就是使用的 UDP 協(xié)議谬泌。通信過程如下:
下面是一個(gè)具體的通信例子滔韵。
1. UDP 服務(wù)器端
最后的 recvfrom 函數(shù)是從指定的地址接收 UDP 數(shù)據(jù),與 recv 的作用基本相同掌实。
// udp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
int serveraddr_len = sizeof(server_addr);
// 1.create socket
int server_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 2.bind
bind(server_fd, (struct sockaddr*)&server_addr, serveraddr_len);
// 3.recv data
char buf[100];
recvfrom(server_fd, buf, 100, 0,
(struct sockaddr*)&server_addr, &serveraddr_len);
printf("UDP server get data from client: %s\n", buf);
// 4.close
close(server_fd);
return 0;
}
2. UDP 客戶端
sendto 函數(shù)是把 UDP 數(shù)據(jù)發(fā)送給指定的地址陪蜻,詳細(xì)使用方法參考 man sendto
。
// udp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char* argv[]) {
struct hostent* myhost = gethostbyname(argv[1]);
struct sockaddr_in client_addr;
client_addr.sin_family = AF_INET;
client_addr.sin_addr = *((struct in_addr*)(myhost->h_addr));
client_addr.sin_port = htons(8888);
// 1.socket
int server_fd = socket(AF_INET, SOCK_DGRAM, 0);
// 2.send data
sendto(server_fd, "Hello World", 11, 0,
(struct sockaddr*)&client_addr, sizeof(client_addr));
printf("UDP client send data ok.\n");
close(server_fd);
return 0;
}
3. 測試 UDP 通信
先來編譯:
gcc -Wall udp_server.c -o udp_server
gcc -Wall udp_client.c -o udp_client
這里會(huì)出現(xiàn)些警告贱鼻,我們暫時(shí)忽略宴卖,因?yàn)椴挥绊懽詈蟮慕Y(jié)果,但是在實(shí)際工作中還是要注意警告邻悬!再來運(yùn)行 UDP 服務(wù)器端:
./udp_server
接著新開一個(gè)終端運(yùn)行 UDP 客戶端:
./udp_client localhost
UDP client send data ok.
數(shù)據(jù)包發(fā)送完成症昏,回到 UDP 服務(wù)器端:
UDP server get data from client: Hello World
服務(wù)器端也成功接收了數(shù)據(jù)了,通信成功啦父丰!
結(jié)語
這篇博客主要介紹了使用 Socket 提供的 API 進(jìn)行 TCP 和 UDP 網(wǎng)絡(luò)通信的基本方法肝谭,并實(shí)際介紹了 2 個(gè) demo,把這兩個(gè) demo 弄清楚了蛾扇,基本的 TCP攘烛,UDP 原理也就理解的差不多了,但在實(shí)際工作中我們主要還是使用優(yōu)秀的開源網(wǎng)絡(luò)庫镀首,一般不會(huì)自己封裝坟漱。網(wǎng)絡(luò)通信是一個(gè)很大的主題,很多細(xì)節(jié)沒有介紹到蘑斧,有興趣可以查看 「計(jì)算機(jī)網(wǎng)絡(luò)」 和 「unix 網(wǎng)絡(luò)編程」 這兩本經(jīng)典書籍靖秩。
那我們下次見须眷,謝謝你的閱讀 :)