Linux網(wǎng)絡(luò)編程篇(一)之Socket 編程預(yù)備知識

Linux系統(tǒng)的一大特點是它的網(wǎng)絡(luò)編程能力十分強大, 學(xué)習(xí)它, 讓我們真正體會網(wǎng)絡(luò)的魅力!

一. 客戶機/服務(wù)器模型

網(wǎng)絡(luò)應(yīng)用程序一般是以c/s模型的方式工作的,因特網(wǎng)便是c/s模型的一個典型例子,在這種工作方式中,一個服務(wù)器通常事先啟動,并在一個熟知端口幀聽對服務(wù)器的請求,如ftp服務(wù)器,web服務(wù)器等.當(dāng)客戶機應(yīng)用程序需要某種服務(wù)時,需向提供這個服務(wù)的服務(wù)器發(fā)出請求,服務(wù)器收到請求后,向客戶機發(fā)出相應(yīng)請求服務(wù).這樣客戶機應(yīng)用程序和服務(wù)器程序之間就建立了通信連接,此后便可以進行數(shù)據(jù)通信,通信任務(wù)完成后,需要關(guān)閉它們之間的通信連接.

C_S_chart_flow

二. 網(wǎng)絡(luò)套接字(socket)介紹

在網(wǎng)路中要全局的標(biāo)示一個參與通信的進程,需要采用三元組: 協(xié)議, 主機ip地址,端口號.要描述兩個應(yīng)用進程之間的端到端的通信則需要一個五元組: 協(xié)議,信源機ip地址,信源應(yīng)用進程端口, 信宿機ip地址,信宿應(yīng)用進程端口.那么從程序設(shè)計的角度如何實現(xiàn)兩個應(yīng)用進程的通信連接的建立,并如何實現(xiàn)兩個進程指佳釀數(shù)據(jù)傳輸呢?人們引入套接字(Socket)的概念.

Socket_flow
  • 套接字實現(xiàn)了對網(wǎng)絡(luò)和傳輸層協(xié)議的封裝
  • 套接字可以看做是處于不同主機之間的兩個進程的通信連接端點
  • 在實現(xiàn)兩個進程間的通信時, 首先應(yīng)用進程各自創(chuàng)建自己的套接字,然后通過套接字建立雙方的通信鏈路,進而利用各自的套接字進行數(shù)據(jù)的發(fā)送個接收

socket這個詞可以表示很多概念:

  • 在TCP/IP協(xié)議中,“IP地址+TCP或UDP端口號”唯一標(biāo)識網(wǎng)絡(luò)通訊中的一個進程,“IP地址+端口號”就稱為socket。

  • 在TCP協(xié)議中,建立連接的兩個進程各自有一個socket來標(biāo)識,那么這兩個socket組成的socket pair就唯一標(biāo)識一個連接。socket本身有“插座”的意思,因此用來描述網(wǎng)絡(luò)連接的一對一關(guān)系

  • TCP/IP協(xié)議最早在BSD UNIX上實現(xiàn),為TCP/IP協(xié)議設(shè)計的應(yīng)用層編程接口稱為socket API

Socket進一步介紹:
socket是使用標(biāo)準(zhǔn)unix文件描述符(file descriptor)和其他程序通訊的方式. Unix中的一切都是文件,接觸過Unix/Linux,就一定會聽過這句話. 實際上, unix程序在執(zhí)行任何形式的I/O時,程序都是在讀或者寫一個文件描述符. 一個文件描述符只是一個跟打開的文件相關(guān)聯(lián)的整數(shù), 這個文件可能是一個網(wǎng)絡(luò)連接, FIFO, 管道, 終端, 文件或者什么其他東西. 所以你要和網(wǎng)絡(luò)上的其他程序通信時,你就要用到文件描述符, 那怎么得到網(wǎng)絡(luò)通信的文件描述符呢?
利用系統(tǒng)調(diào)用socket((),它會返回套接字描述符(socket descriptor), 然后就可以用它來send(), recv() ,發(fā)送接收數(shù)據(jù).

三. 套接字編程基礎(chǔ)

Tcp/Ip的核心內(nèi)容被封裝在操作系統(tǒng)中,網(wǎng)絡(luò)應(yīng)用程序要使用tcp/ip來實現(xiàn)自己的功能,需要通過操作系統(tǒng)提供給用戶的編程借口來實現(xiàn). 套接字就是Tcp/Ip網(wǎng)絡(luò)編程接口的集合,他是應(yīng)用程序預(yù)tcp/ip協(xié)議族通信的中間軟件抽象層.

1. socket

// socket - create an endpoint for communication

#include <sys/types.h>          
#include <sys/socket.h>

int socket(int domain, int type,int protocol)

// DESCRIPTION
//       socket() creates an endpoint for communication and returns a file descriptor that 
//       refers to that endpoint.  The file descriptor returned by a successful call will 
//       be the lowest-numbered file descrip‐tor not currently open for the process.

domain: 說明我們網(wǎng)絡(luò)程序所在的主機采用的通訊協(xié)族(AF_UNIX 和 AF_INET 等). AF_UNIX 只能夠用于單一的 Unix 系統(tǒng)進程間通信,而 AF_INET 是針對 Internet 的,因而可以允許在遠程 主機之間通信(當(dāng)我們 man socket 時發(fā)現(xiàn) domain 可選項是 PF_而不是 AF_,因為glibc 是 posix 的實現(xiàn) 所以用 PF 代替了 AF,不過我們都可以使用的).

type: 我們網(wǎng)絡(luò)程序所采用的通訊協(xié)議(SOCK_STREAM,SOCK_DGRAM 等)

  • SOCK_STREAM (流套接字) 表明我們用的是 TCP 協(xié)議,這樣會提供按順序的,可靠,雙向,面向連接的比特流.

  • SOCK_DGRAM (數(shù)據(jù)包套接字) 表明我們用的是 UDP 協(xié)議,這樣只會提供定長的,不可靠,無連接的通信.

  • SOCK_RAW (原始套接字) 表明這是個原始套接字, 相對與上面兩種類型, 提供了更多的功能, 實現(xiàn)ping/traceruoute等均需要創(chuàng)建此類套接字

    網(wǎng)絡(luò)層次 Operation SOCK_STREAM / SOCK_DGRAM SOCK_ROW
    應(yīng)用層(Application Layer) telnet, ftp, http, dns...
    傳輸層(Transport Layer) TCP, UDP √(數(shù)據(jù)部分)
    網(wǎng)絡(luò)層(Internet Layer) IP ×
    數(shù)據(jù)鏈路層(Data link) MAC ×

    鏈路層的原始套接字可以直接用于接收和發(fā)送鏈路層的MAC幀,在發(fā)送時需要由調(diào)用者自行構(gòu)造和封裝MAC首部
    網(wǎng)絡(luò)層的原始套接字可以直接用于接收和發(fā)送IP層的報文數(shù)據(jù),在發(fā)送時需要自行構(gòu)造IP報文頭

    一般的套接字只能操作傳輸層的數(shù)據(jù)部分的內(nèi)容, 我們只能將發(fā)送的數(shù)據(jù)(buffer)傳遞給系統(tǒng), 系統(tǒng)幫我們給數(shù)據(jù)加上tcp/udp頭部,再加上ip頭部, 再給發(fā)出去; 而使用原始套接字 需要我們自己構(gòu)造每個部分, 系統(tǒng)只是將它發(fā)出去, 想一想, 這是不是多了好多樂趣呢 ^_^

    注: 原始套接字需要root權(quán)限, 針對具體情況使用相應(yīng)套接字類型, 推薦閱讀書籍^[UNIX網(wǎng)絡(luò)編程 卷1:套接字聯(lián)網(wǎng)API]

protocol :由于我們指定了 type,所以這個地方我們一般只要用 0 來代替就可以了, socket 為網(wǎng)絡(luò)通訊做基本的準(zhǔn)備. 如果我們使用的是原始套接字,這個時候系統(tǒng)是不知道你要發(fā)送什么類型數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù), 這時候就需要指定協(xié)議類型, 如 IPPROTO_ICMP, IPPROTO_TCP, IPPROTO_UDP等.

Return Value : 成功時返回文件描述符,失敗時返回-1,看 全局變量 errno 可知道出錯的詳細情況

// 常用方式 下同
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
    fprintf(stderr,"Create Socket Error, %s\n", perror(errno);
    exit(errno);
}

2. bind

// bind - bind a name to a socket
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

sockfd: 是由 socket 調(diào)用返回的文件描述符.

addrlen: 是 sockaddr 結(jié)構(gòu)的長度.

my_addr: 是一個指向結(jié)構(gòu)體 sockaddr 的指針,它保存你的地址(即端口和 IP 地址) 信息。 在<linux/socket.h>中有 sockaddr 的定義

struct sockaddr{
        unisgned short as_family;
        char sa_data[14];
};

不過由于系統(tǒng)的兼容性,我們一般不用這個頭文件,而使用另外一個結(jié)構(gòu)(struct sockaddr_in) 來代替.在<linux/in.h>中有 sockaddr_in 的定義

struct sockaddr_in{
        unsigned short sin_family;
        unsigned short int sin_port;
        struct in_addr sin_addr;
        unsigned char sin_zero[8];
}

注: 由于sockaddr數(shù)據(jù)結(jié)構(gòu)與sockaddr_in數(shù)據(jù)結(jié)構(gòu)的大小是相同的,指向sockaddr_in的指針可以通過強制類型轉(zhuǎn)換,轉(zhuǎn)換成指向sockaddr結(jié)構(gòu)的指針

我們主要使用 Internet 所以 sin_family 一般為 AF_INET,

sin_addr.s_addr 設(shè)置為 INADDR_ANY 表示自動填上所運行的機器的ip地址,

sin_port 是我們要監(jiān)聽的端口號, 賦值0則告訴系統(tǒng)自動選擇端口

sin_zero[8]是用來填充的

在這里插入圖片描述

bind 將本地的端口同 socket 返回的文件描述符捆綁在一起.成功是返回 0,失敗的情況和socket 一樣
簡單例子:

#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#define _INT_PORT 9257

int main(void)
{
    int sockfd;                                             // 定義套接字
    struct sockaddr_in my_addr;                             // 定義存儲本地地址信息的結(jié)構(gòu)體
    sockfd = socket(PF_INET, SOCK_STREAM, 0);               // 創(chuàng)建套接字
    
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(_INT_PORT);
    my_addr.sin_addr.s_addr = inet_addr("132.241.5.10");
    
    bzero(&(my_addr.sin_zero), sizeof(my_addr.sin_zero));
    if(bind(sockfd, (struct sockaddr *)&my_addr,sizeof(struct sockaddr_in)) == -1){         // 綁定套接字
        fprintf(stderr, "Bind socket error, %s\n", perror(errno));
        exit(errno);
    }
}

這個過程就是指定程序綁定到系統(tǒng)的某一個端口, 試想一下, 這個bind過程 是不是必須的呢?

3. listen

// listen - listen for connections on a socket
#include <sys/types.h>          
#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:是 bind 后的文件描述符.

backlog:設(shè)置請求排隊的最大長度.當(dāng)有多個客戶端程序和服務(wù)端相連時, 使用這個表示可以連接的最大長度.

listen 函數(shù)將 bind 的文件描述符變?yōu)楸O(jiān)聽套接字.返回的情況和 bind 一樣.在發(fā)生錯誤的時候返回-1,并設(shè)置全局錯誤變量 errno

以上的過程就是, 創(chuàng)建一個套接字, 綁定本地的地址信息, 然后在 Listen, 監(jiān)聽這個端口 , 等待別人來連接, 而我們用下面的accept來接收別人的連接

4. accept

// 在發(fā)生錯誤的時候返回-1,并設(shè)置全局錯誤變量 errno
#include <sys/types.h>          
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr,int *addrlen)

sockfd:是 listen 后的文件描述符.

addr,addrlen 是用來給客戶端的程序填寫的,服務(wù)器端只要傳遞指針就可以了.
bind,listen 和 accept 是服務(wù)器端用的函數(shù),
accept 調(diào)用時,服務(wù)器端的程序會一直阻塞到有一個客戶程序發(fā)出了連接. accept 成功時返回最后的服務(wù)器端的文件描述符,這個時候服務(wù)器端可以通過該描述符進行send() 和 recv()操作. 失敗時返回-1

具體代碼感受一下:

#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#define SET_PORT 3490
int main(void)
{
    int sockfd, new_fd;
    struct sockaddr_in my_addr;
    struct sockaddr_in their_addr;
    int sin_size;
    
    sockfd = socket(PF_INET, SOCK_STREAM, 0);
    
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(_INT_PORT);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    
    bzero(&(my_addr.sin_zero),sizeof(my_addr.sin_zero));
    bind(sockfd, (struct sockaddr *)&my_addr,sizeof(struct sockaddr));// 綁定套接字
    
    listen(sockfd, 10);                                                     // 監(jiān)聽套接字
    
    sin_size = sizeof(struct sockaddr_in);
    new_fd = accept(sockfd, &their_addr, &sin_size);                        // 接收套接字
}

5. connect

// connect - initiate a connection on a socket
#include <sys/types.h>          
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)

sockfd:socket 返回的文件描述符.

serv_addr:儲存了服務(wù)器端的地址信息(IP地址, 端口號)的數(shù)據(jù)結(jié)構(gòu).其中 sin_add 是服務(wù)端的地址

addrlen:serv_addr 的長度 sizeof(struct sockaddr);

connect 函數(shù)是客戶端用來同服務(wù)端連接的.成功時返回 0,sockfd 是同服務(wù)端通訊的文件描述符 失敗時返回-1.

我們想一想,我們連接服務(wù)器,是不是只需要在套接字中提供服務(wù)器端的ip地址和端口號即可, 不需要指定本地的ip地址和端口號, 也即這個時候使用bind()來綁定客戶端的地址信息,這個操作不是必須的, 系統(tǒng)會自動獲取本地ip, 自動為該套接字分配端口號

6. close & shutdown

如果你已經(jīng)整天都在發(fā)送 (send()) 和接收 (recv()) 數(shù)據(jù)了,現(xiàn)在你準(zhǔn)備關(guān) 閉你的套接字描述符了。

這很簡單,你可以使用一般的 Unix 文件描述符 的 close() 函數(shù):

close(sockfd);它將防止套接字上更多的數(shù)據(jù)的讀寫。任何在另一端讀寫套接字的企圖都將返回錯誤信息。

如果你想在如何關(guān)閉套接字上有多一點的控制,你可以使用函數(shù) shutdown()。

它允許你將一定方向上的通訊或者雙向的通訊(就象 close()一 樣)關(guān)閉,你可以使用:
int shutdown(int sockfd, int how);
sockfd 是你想要關(guān)閉的套接字文件描述復(fù)。how 的值是下面的其中之 一:
0 - 不允許接受
1 - 不允許發(fā)送
2 - 不允許發(fā)送和接受(和 close() 一樣)
shutdown() 成功時返回 0,失敗時返回 -1(同時設(shè)置 errno。) 如果在無連接的數(shù)據(jù)報套接字中使用 shutdown(),那么只不過是讓 send() 和 recv() 不能使用(記住你在數(shù)據(jù)報套接字中使用了 connect 后 是可以使用它們的)

四.相關(guān)常用函數(shù)

1. 網(wǎng)絡(luò)字節(jié)順序(Network Byte Order)

內(nèi)存中的多字節(jié)數(shù)據(jù)相對于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對于文件中的偏移地址也
有大端小端之分。網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分,那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?發(fā)送主機通常將發(fā)送
緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機把從網(wǎng)絡(luò)上接到的字節(jié)依次保存在接收緩沖區(qū)中,
也是按內(nèi)存地址從低到高的順序保存,因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出
的數(shù)據(jù)是高地址。

Tcp/Ip協(xié)議規(guī)定, 網(wǎng)絡(luò)傳輸字節(jié)順序為高位優(yōu)先,為了使網(wǎng)絡(luò)程序具有可移植性, 需要對程序?qū)⒈镜刈止?jié)順序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)順序, 可以調(diào)用庫函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機字節(jié)序(Host Byte Order)的轉(zhuǎn)換, 常用函數(shù)如下:

    #include<arpa/inet.h>

    uint32_t htonl(uint32_l hostlong);      // "Host to Network Long"
    uint16_t htons(uint16_t hostshort);     // "Host to Network Short"
    uint32_t ntohl(uint32_t netlong);           // "Network to Host Long"
    uint16_t ntohs(uint16_t netshort);      // "Network to Host Short"
  • 問: 如果我的主機使用的就是網(wǎng)絡(luò)字節(jié)順序, 為什么還要調(diào)用 htonl() 轉(zhuǎn)換 IP 地址呢?
    因為將你的程序移植到別人的電腦上可能會報錯(具備可移植性). 記住:在你將數(shù)據(jù)放到網(wǎng)絡(luò)上的時候,確信它們是網(wǎng)絡(luò)字節(jié)順序的!
  • 問: 為什么在數(shù)據(jù)結(jié)構(gòu) struct sockaddr_in 中, sin_addr 和 sin_port 需要轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)順序,而 sin_family 需不需要呢?
    答案是:sin_addr 和 sin_port分別封裝在包的 IP 和 UDP 層。因此,它們必須要 是網(wǎng)絡(luò)字節(jié)順序。但是 sin_family域只是被內(nèi)核 (kernel) 使用來決定在數(shù) 據(jù)結(jié)構(gòu)中包含什么類型的地址,所以它必須是本機字節(jié)順序。同時, sin_family 沒有發(fā)送到網(wǎng)絡(luò)上,它們可以是本機字節(jié)順序。

2. Ip地址處理轉(zhuǎn)換函數(shù)

我們有很多的函數(shù)來方便地操作 IP 地址。沒有必要用手工計算它們,也沒有必要用"<<"操作來儲存成長整字型.
假如我們現(xiàn)在有一個sockaddr_Ser 的地址結(jié)構(gòu)體, 要將ip地址"103.21.187.5"保存到該結(jié)構(gòu)體中, 我們可以用

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

sockaddr_Ser.sin_addr.s_addr = inet_addr("103.21.187.5");

注意, inet_addr()返回的地址已經(jīng)是網(wǎng)絡(luò)字節(jié)格式,所以你無需再調(diào)用 函數(shù) htonl()。

那如何講一個長整型的ip地址轉(zhuǎn)為點分十進制呢?

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

printf("%s", inet_ntoa(sockaddr_Clinet.sin_addr));

inet_ntoa() 含義 network to ascii .
需要注意的是 inet_ntoa()將結(jié)構(gòu)體 in-addr 作為一 個參數(shù),不是長整形
同樣需要注意的是它返回的是一個指向一個字符的指針。它是一個由 inet_ntoa()控制的靜態(tài)的固定的指針,所以每次調(diào)用 inet_ntoa(),它就將覆蓋上次調(diào)用時所得的IP 地址

3. gethostbyname()

通過給定的名字, 找到相應(yīng)的主機信息, 如給定域名,查找返回ip地址, 進行bind, connect等操作.

#include <netdb.h>

struct hostent *gethostbyname(const char *name);
// 很明白的是,它返回一個指向 struct hostent 的指針。這個數(shù)據(jù)結(jié)構(gòu) 是這樣的:

struct hostent
{
    char *h_name;           // 地址的正式名稱
    char **h_aliases;   // 空字節(jié)-地址的預(yù)備名稱的指針
    int h_addrtype;     // 地址類型; 通常是 AF_INET
    int h_length;           // 地址的比特長度
    char **h_addr_list; // 零字節(jié)-主機網(wǎng)絡(luò)地址指針。網(wǎng)絡(luò)字節(jié)順序
};
#define h_addr h_addr_list[0]

gethostbyname() 成功時返回一個指向結(jié)構(gòu)體 hostent 的指針,或者 是個空 (NULL)指 針 。 ( 但 是 和 以 前 不 同 , 不 設(shè) 置 errno , h_errno 設(shè) 置 錯 誤 信 息 。)

參照代碼例子, 加快理解:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char* argv[]){
    struct hostent* ht;
    char** pptr;
    int type;
    if(argc != 2){
        fprintf(stderr, "usage: ./filename [address]\n");
        exit(EXIT_FAILURE);
    }
    if(!(ht = gethostbyname(argv[1]))){
        herror("main gethostbyname error");
        exit(EXIT_FAILURE);
    }
    //打印所有信息
    printf("Host name is: %s\n", ht->h_name);
    //打印所有的主機地址
    for(pptr=ht->h_aliases; (*pptr); ++pptr)
        printf("alias of host: %s\n", *pptr);
    printf("Host addrtype is: %d\n", type = ht->h_addrtype);
    printf("Host length is: %d\n", ht->h_length);
    if(type==AF_INET || type==AF_INET6){
        char ip[32];
        for(pptr = ht->h_addr_list; (*pptr); ++pptr){
            inet_ntop(type, *pptr, ip, sizeof ip);
            printf("address: %s\n", ip);
        }
    }
    return 0;
}

五. 消息發(fā)送與接收

前面我們已經(jīng)講了如何創(chuàng)建,綁定, 監(jiān)聽, 連接套接字的操作, 可能等不及的同學(xué)已經(jīng)開始想, 那怎么通過套接字發(fā)送數(shù)據(jù)呢. 這一小節(jié)我們就來講消息發(fā)送接收的函數(shù)調(diào)用.

在這之前,附上一張簡單但很重要的流程圖, 縱使這系列后面的內(nèi)容說的再天花亂墜, 實質(zhì)上也是這圖上的流程. 特別是剛起步的同學(xué), 記住這張圖, 就把握了整體的流程脈絡(luò), 不至于代碼無從敲起.

socket通信CS模式示意圖

1. send() 和 recv() 函數(shù)

這兩個函數(shù)用于流式套接字或者數(shù)據(jù)報套接字的通訊。

#include <sys/socket.h>

// send — send a message on a socket
ssize_t send(int socket, const void *buffer, size_t length, int flags);

// recv — receive a message from a connected socket
ssize_t recv(int socket, void *buffer, size_t length, int flags);

socket 是你想發(fā)送數(shù)據(jù)的套接字描述符(或者是調(diào)用 socket() 或者是 accept() 返回的 )
buffer 是指向你想發(fā)送的數(shù)據(jù)的指針。
length 是數(shù)據(jù)的長度。
把 flags 設(shè)置為 0 就可以了, 剛開始用不上(詳細的資料請看 send() 的 man page)

send() 返回實際發(fā)送的數(shù)據(jù)的字節(jié)數(shù)--它可能小于你要求發(fā)送的數(shù) 目!
注意,有時候你告訴它要發(fā)送一堆數(shù)據(jù)可是它不能處理成功。它只是 發(fā)送它可能發(fā)送的數(shù)據(jù),然后希望你能夠發(fā)送其它的數(shù)據(jù)。記住,如果 send() 返回的數(shù)據(jù)和 len 不匹配,你就應(yīng)該發(fā)送其它的數(shù)據(jù)。
最后要說得就是,它在錯誤的時候返回-1,并設(shè)置 errno。

recv() 函數(shù)很類似
socket 是要讀的套接字描述符。
buffer 是要讀的信息的緩沖。
length 是緩 沖的最大長度。
flags 可以設(shè)置為 0。(請參考 recv() 的 man page)

recv() 返回實際讀入緩沖的數(shù)據(jù)的字節(jié)數(shù)?;蛘咴阱e誤的時候返回-1, 同時設(shè)置 errno。

2. sendto() 和 recvfrom() 函數(shù)

這兩個函數(shù)用于 無連接數(shù)據(jù)報套接字

#include <sys/socket.h>

ssize_t sendto(int socket, const void *buffer, size_t length, int flags, 
           const struct sockaddr *dest_addr,socklen_t dest_len);

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
           int flags, struct sockaddr *restrict address,
           socklen_t *restrict address_len);

可以看到,sendto() 函數(shù)除了另外的兩個信息外,其余的和函數(shù) send() 是一樣 的。
dest_addr 是個指向數(shù)據(jù)結(jié)構(gòu) struct sockaddr 的指針,它包含了目的地的 IP 地址和端口信息
dest_len 可以簡單地設(shè)置為 sizeof(struct sockaddr)。
和函數(shù) send() 類似, sendto() 返回實際發(fā)送的字節(jié)數(shù)(它也可能小于 你想要發(fā)送的字節(jié)數(shù)!),或者在錯誤的時候返回 -1。

recvfrom() 函數(shù)也是類似, 除了兩個增加的參數(shù)外,這個函數(shù)和 recv() 也是一樣的。
address 是一個指向局部數(shù)據(jù)結(jié)構(gòu) struct sockaddr 的指針,它的內(nèi)容是請求連接方的 IP 地址和端口信息。
address_len 是個 int 型的局部指針,它的初始值為 sizeof(struct sockaddr)。
函數(shù)調(diào)用返回后, address_len 保存著實際儲存在 address 中的地址的長度。
recvfrom() 返回收到的字節(jié)長度,或者在發(fā)生錯誤后返回 -1。

問: 為什么udp連接的套接字函數(shù)(sendto, recvfrom) 相比與tcp的通信的套接字,要提供更多的參數(shù)呢?
答: 這是因為tcp是面向連接的協(xié)議,在雙方通信之前,是已經(jīng)建立好了連接, 直接send數(shù)據(jù), 便可發(fā)到連接另一端的消息接收方;
而udp是無連接的協(xié)議, 通信之間沒有建立連接, 所以在發(fā)送消息時, 必須指明消息接收方的地址信息(IP地址及端口號), 才能將消息發(fā)送給對方.

注: 如果你用 connect() 連接一個數(shù)據(jù)報套接字,你可以簡單的調(diào) 用 send() 和recv() 來滿足你的要求。這個時候依然是數(shù)據(jù)報套接字,依 然使用 UDP,系統(tǒng)套接字接口會為你自動加上了目標(biāo)和源的信息

原來是不想貼代碼的, 但怕有些人看完上面的內(nèi)容,急不可耐的向感受一下, 還是給附上一個最簡單的demo:
更多代碼移步我github: https://github.com/D-lyw/Socket_C_linux 查看相關(guān)代碼: tcp, udp, ping, tracert, dns, ftp.....

tcp_server.c

/*
 * @Author: D-lyw 
 * @Date: 2018-10-25 00:48:44 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-16 12:29:37
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>

#define SERVADDR_PORT 8800

const char *LOCALIP = "127.0.0.1";

int main(int argc, char const *argv[])
{
    // 定義變量存儲生成或接收的套接字描述符
    int listenfd, recvfd;
    // 定義一個數(shù)據(jù)結(jié)構(gòu)用來存儲套接字的協(xié)議,ip,端口等地址結(jié)構(gòu)信息
    struct sockaddr_in servaddr, clientaddr;
    // 定義接收的套接字的數(shù)據(jù)結(jié)構(gòu)的大小
    unsigned int cliaddr_len, recvLen;
    char recvBuf[1024];

    //創(chuàng)建用于幀聽的套接字
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    // 給套接字數(shù)據(jù)結(jié)構(gòu)賦值,指定ip地址和端口號
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVADDR_PORT);
    servaddr.sin_addr.s_addr = inet_addr(LOCALIP);

    // 綁定套接字
    if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){
        fprintf(stderr, "綁定套接字失敗,%s\n", strerror(errno));
        exit(errno);
    }

    // 監(jiān)聽請求
    if(listen(listenfd, 10) == -1){
        fprintf(stderr, "綁定套接字失敗,%s\n", strerror(errno));
        exit(errno);
    }

    cliaddr_len = sizeof(struct sockaddr);

    // 等待連接請求
    while (1){
        // 接受由客戶機進程調(diào)用connet函數(shù)發(fā)出的連接請求
        recvfd = accept(listenfd, (struct sockaddr *)&clientaddr, &cliaddr_len);
        printf("接收到請求套接字描述符: %d\n", recvfd);

        while(1){
            // 在已建立連接的套接字上接收數(shù)據(jù)
            if((recvLen = recv(recvfd, recvBuf, 1024, 0)) == -1){
                fprintf(stderr,"接收數(shù)據(jù)錯誤, %s\n",strerror(errno));
            }
            printf("%s", recvBuf);
        }
    }
    close(recvfd);
    return 0;
}

tcp_client.c

/*
 * @Author: D-lyw 
 * @Date: 2018-10-26 14:06:32 
 * @Last Modified by: D-lyw
 * @Last Modified time: 2018-11-16 12:34:08
 * @name tcp_client.c
 * @descripe    實現(xiàn)最基本的創(chuàng)建套接字, 填充客戶端信息,connet連接服務(wù)端, 可連續(xù)向服務(wù)端發(fā)送消息
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <string.h>
extern int errno;

#define SERVERPORT 8800

int main(int argc, char const *argv[])
{
    // 定義變量存儲本地套接字描述符
    int clifd;
    // 設(shè)置本地ip地址
    const char serverIp[] = "127.0.0.1";
    // 定義套接字結(jié)構(gòu)存儲套接字的ip,port等信息
    struct sockaddr_in cliaddr_in;
    // 定義發(fā)送,接收緩沖區(qū)大小
    char sendBuf[1024], recvBuf[1024];

    // 創(chuàng)建套接字
    if((clifd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        fprintf(stderr, "創(chuàng)建套接字失敗,%s\n", strerror(errno));
        exit(errno);
    }

    // 填充 服務(wù)器端結(jié)構(gòu)體信息
    cliaddr_in.sin_family = AF_INET;
    cliaddr_in.sin_addr.s_addr = inet_addr(serverIp);
    cliaddr_in.sin_port = htons(SERVERPORT);

    // 請求連接服務(wù)器進程
    if(connect(clifd, (struct sockaddr *)&cliaddr_in, sizeof(struct sockaddr)) == -1){
        fprintf(stderr,"請求連接服務(wù)器失敗, %s\n", strerror(errno));
        exit(errno);
    }
    strcpy(sendBuf, "hi,hi, severs!\n");
    // 發(fā)送打招呼消息
    if(send(clifd, sendBuf, 1024, 0) == -1){
        fprintf(stderr, "send message error:(, %s\n", strerror(errno));
        exit(errno);
    }
    // 阻塞等待輸入,發(fā)送消息
    while(1){
        fgets(sendBuf, 1024, stdin);
        if(send(clifd, sendBuf, 1024, 0) == -1){
            fprintf(stderr, "send message error:(, %s\n", strerror(errno));
        }
    }
    close(clifd);
    return 0;
}

終于寫完了, 如果覺得寫的還可以,請點個贊 :)...
這是Linux網(wǎng)絡(luò)編程系列的第一篇文章, 感興趣的同學(xué), 可瀏覽該系列的其他文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 套接字是網(wǎng)絡(luò)通信的基石,是網(wǎng)絡(luò)通信的基本構(gòu)建,最初是由加利福利亞大學(xué)Berkeley分校為UNIX開發(fā)的網(wǎng)絡(luò)通信編...
    Super超人閱讀 4,743評論 0 26
  • 第一章 TCP/IP簡介 基本的C/S服務(wù)模型 網(wǎng)絡(luò)編程是指編寫的網(wǎng)絡(luò)通信程序可以與網(wǎng)絡(luò)上的其他程序進行通信。 T...
    Waldo_cuit閱讀 1,989評論 0 6
  • 網(wǎng)絡(luò)中進程之間如何通信 為了方便大家獲取源代碼,可以移步這里,GitHub源代碼 進程通信的概念最初來源于單機系統(tǒng)...
    batbattle閱讀 14,253評論 1 5
  • 簡介 Socket理論 Socket工作流程 核心函數(shù)講解 服務(wù)的如何獲取客戶端的信息 字符串ip和網(wǎng)絡(luò)二進制的轉(zhuǎn)...
    第八區(qū)閱讀 4,092評論 0 4
  • 每個人都有一股氣含在口里憋在心里憋久了,會在體內(nèi)結(jié)石放出來,又惹人不快在擁擠的地方格外容易漏氣在人群中經(jīng)常感覺到一...
    艾黑丫閱讀 263評論 9 21

友情鏈接更多精彩內(nèi)容