從 0 開始學(xué)習(xí) Linux 系列之「27.Socket 編程基礎(chǔ)(TCP殖蚕,UDP)」

版權(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 通信的過程:

tcp socket

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ò)的地方:

  1. INADDR_ANY: 允許任何 IP 地址的客戶端連接本服務(wù)器
  2. 指定端口為 8888: 連接的端口被指定為 8888昨稼,如果連接不成功則端口可能被占用,可以嘗試更換端口
  3. AF_INETSOCK_STREAM: 兩者確定了當(dāng)前使用的是 TCP 協(xié)議
  4. 強(qiáng)制轉(zhuǎn)換為 struct sockaddr *: 在使用 bind 和 accept 時(shí)拳锚,都需要將 sockaddr_insockaddr_un 強(qiáng)制轉(zhuǎn)換為這個(gè)類型
  5. 不要忘記關(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é)議谬泌。通信過程如下:

udp socket

下面是一個(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)典書籍靖秩。

那我們下次見须眷,謝謝你的閱讀 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末竖瘾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子花颗,更是在濱河造成了極大的恐慌捕传,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扩劝,死亡現(xiàn)場離奇詭異庸论,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)棒呛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帆啃,你說我怎么就攤上這事擒悬。” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵扛禽,是天一觀的道長锋边。 經(jīng)常有香客問我,道長编曼,這世上最難降的妖魔是什么豆巨? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮掐场,結(jié)果婚禮上往扔,老公的妹妹穿的比我還像新娘。我一直安慰自己熊户,他們只是感情好瓤球,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敏弃,像睡著了一般卦羡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上麦到,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天绿饵,我揣著相機(jī)與錄音,去河邊找鬼瓶颠。 笑死拟赊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粹淋。 我是一名探鬼主播吸祟,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼桃移!你這毒婦竟也來了屋匕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤借杰,失蹤者是張志新(化名)和其女友劉穎过吻,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔗衡,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纤虽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绞惦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逼纸。...
    茶點(diǎn)故事閱讀 39,932評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖济蝉,靈堂內(nèi)的尸體忽然破棺而出杰刽,到底是詐尸還是另有隱情呻纹,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布专缠,位于F島的核電站雷酪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏涝婉。R本人自食惡果不足惜哥力,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望墩弯。 院中可真熱鬧吩跋,春花似錦、人聲如沸渔工。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽引矩。三九已至梁丘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旺韭,已是汗流浹背氛谜。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留区端,地道東北人值漫。 一個(gè)月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像织盼,于是被迫代替她去往敵國和親杨何。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內(nèi)容