1. 程式人生 > 其它 >Linux 系統程式設計 學習:7-基於socket的網路程式設計2:基於 UDP 的通訊

Linux 系統程式設計 學習:7-基於socket的網路程式設計2:基於 UDP 的通訊

知識

UDP:User Datagram Protocol的縮寫。
UDP不提供複雜控制機制,利用IP提供面向無連線的通訊服務。且它是將應用程式發來的資料在收到的那一刻,立即按照原樣傳送到網路上的一種機制。

UDP面向無連線,可以隨時傳送資料。它常用於幾個方面:

  • 包總量較少的通訊(DNS、SNMP等)
  • 視訊、音訊等多媒體通訊(即時通訊)
  • 限定於LAN等特定網路中的應用通訊
  • 廣播通訊(廣播、多播)

典型的 UDP 通訊流程圖如下:

ServerClient雙方都建立socket物件socket()socket()伺服器一般繫結埠號bind()收發訊息sendto()/recvfrom()sendto()/recvfrom()關閉連線close()close()ServerClient

有關函式介紹

根據流程圖,我們知道,在UDP通訊中,使用到了這些函式:socket()bind()sendto()recvfrom()

socket

c
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>

intsocket(int domain, int type, int protocol);

描述:建立一個用於網路通訊的套接字。
引數解析

domain:

  • AF_INET:IPv4協議
  • AF_INET6:IPv6協議
  • AF_LOCAL:Unix域協議
  • AF_ROUTE:路由套介面
  • AF_KEY:金鑰套介面

在早期版本中的domain引數中還有以 PF_開頭的定義,當初設計者設計的時候,設想地址使用一套域,協議使用一套域,然後就沒有然後了。所以,實際上,PF_ 其實相當於 AF_

type:

  • SOCK_STREAM:提供面向連線的雙向可靠資料流,對應TCP協議

  • SOCK_DGRAM:提供無保障的面向訊息的雙向不可靠資料報,對應UDP,可用於在網路上發廣播資訊。(UDP)

  • SOCK_RAW:提供傳輸層以下的協議,可以訪問內部網路介面,例如接收和傳送ICMP報文

  • SOCK_RDM:提供可靠的資料報文,但可能資料會有亂序

  • SOCK_SEQPACKET:序列化包,提供一個序列化的、可靠的、雙向的基本連線的資料傳輸通道,資料長度定常。每次呼叫讀系統呼叫時資料需要將全部資料讀出

  • 同時,可以 與下面的值相或

    • SOCK_NONBLOCK . 非阻塞,也可以使用fcntl()來做這個事情
    • SOCK_CLOEXEC 在執行 exec 時關閉該描述符,相當於 FD_CLOEXEC

protocol:當type為SOCK_RAW時需要設定此值說明協議型別,其他型別設定為0即可, 對於protocol為0(IPPROTO_IP)的raw socket。用於接收任何的IP資料包。其中的校驗和和協議分析由程式自己完成

返回值:成功返回socket描述符;失敗返回-1,置errno:

底層協議模組可能會產生其他錯誤。

  • ERRORS:建立指定型別和/或pro-tocol的套接字的許可權被拒絕
  • EAFNOSUPPORT:不支援指定的地址族
  • EINVAL 未知協議,或協議系列不可用
  • EINVAL :無效的type
  • EMFILE :已達到每個程序對開啟的檔案描述符數的限制
  • ENFILE :已達到系統範圍內開啟檔案總數的限制
  • ENOBUFS or ENOMEM :可用記憶體不足。只有釋放足夠的資源,才能建立套接字
  • EPROTONOSUPPORT :此域中不支援協議型別或指定的協議

bind

一個套接字只是使用者程式與核心互動資訊的樞紐,它自身沒有太多的資訊,也沒有網路協議地址和埠號等資訊。進行網路通訊時,必須把一個套接字與一個地址相關聯,這個過程就是地址繫結的過程。
許多時候核心會我們自動繫結一個地址,然而有時使用者可能需要自己來完成這個繫結的過程,以滿足實際應用的需要。

最典型的情況是一個伺服器程序需要繫結一個眾所周知的地址或埠以等待客戶來連線。這個事由bind的函式完成。

伺服器啟動時需要繫結指定的埠來提供服務(以便於客戶向指定的埠傳送請求),對於伺服器socket繫結地址,一般而言將IP地址賦值為INADDR_ANY(該巨集值為0),即無論傳送到系統中的哪個IP地址(當伺服器有多張網絡卡時會有多個IP地址)的請求都採用該socket來處理,而無需指定固定IP。

對於Client,一般而言無需主動呼叫bind(),一切由作業系統來完成。在傳送資料前,作業系統會為套接字隨機分配一個可用的埠,同時將該套接字和本地地址資訊繫結。

一個網路應用程式只能繫結一個埠( 一個套接字只能 繫結一個埠 )。如果需要繫結多個埠,需要使用埠複用(略)。

c
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>

intbind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

/* 可能會用到以下結構體 */
structsockaddr {
   sa_family_t sa_family; // 地址族
   char        sa_data[14]; // 資料
}

/* 一般使用以下結構體代替 */
#include<netinet/in.h>
#include<arpa/inet.h>
structsockaddr_in {
    sa_family_t 	sa_family;	 // 地址族
	uint16_t 		sin_port; 	 // 16位 TCP/UDP 埠號
    structin_addrsin_addr;	 // 32位IP地址,裡面的內容是 In_addr_t
    char			sin_zero[8]; // 不使用
}

描述:將addr指向的sockaddr結構體中描述的一些屬性(IP地址、埠號、地址簇)與socket套接字繫結(也叫給套接字命名)。 為socket套接字關聯了一個相應的地址與埠號,即傳送到地址值該埠的資料可通過socket讀取和使用。

引數解析

sockfd:由socket()函式成功返回的結果

addr: 設定有網路屬性的sockaddr_in或sockaddr 結構體

  • sa_family

addrlen:sockaddr_in或sockaddr 結構體的大小

與 sockaddr_in 有關的函式

常在埠號中使用的:

c
#include<arpa/inet.h>

// 主機位元組序到網路位元組序
u_long htonl(u_long hostlong);
u_short htons(u_short short);

// 網路位元組序到主機位元組序
u_long ntohl(u_long hostlong);
u_short ntohs(u_short short);

常在地址轉換中使用的:

c
#include<arpa/inet.h>

// 將網路地址轉換為 以 點分十進位制表示的字串(字串存在於靜態記憶體中)
char *inet_ntoa(struct in_addr);

// 點分十進位制字串 轉 32位(IPV4 IP)二進位制
in_addr_tinet_addr(constchar* cp);

範例:

c
	...
    structsockaddr_inaddr ={0};
    addr.sin_family = AF_INET; 	  // 地址協議族 
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
    addr.sin_port = htons(12345); //指定埠號
	...

sendto()

關於傳送,有下列這些函式。

send只可用於基於連線(TCP)的套接字,send 和 write唯一的不同點是標誌的存在,當標誌為0時,send等同於write。

sendto 和 sendmsg既可用於無連線的套接字,也可用於基於連線的套接字。除了套接字設定為非阻塞模式,呼叫將會阻塞直到資料被髮送完。

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

ssize_tsend(int sockfd, constvoid *buf, size_t len, int flags);

ssize_tsendto(int sockfd, constvoid *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_tsendmsg(int sockfd, const struct msghdr *msg, int flags);

/* 可能用到的結構體 */
structmsghdr {
   void         *msg_name;       /* optional address */ // 用於無連線套接字的場合
   socklen_t     msg_namelen;    /* size of address */  // 用於無連線套接字的場合
   structiovec *msg_iov;        /* scatter/gather array */
   size_t        msg_iovlen;     /* # elements in msg_iov */
   void         *msg_control;    /* ancillary data, see below */
   size_t        msg_controllen; /* ancillary data buffer len */
   int           msg_flags;      /* flags (unused) */
};

描述:傳送資料到指定的地址

引數解析:

sockfd:要傳送到哪個套接字

buf:要傳送資料首地址

len:要傳送的資料長度

flags:是以下零個或者多個標誌的相或結果

  • MSG_DONTROUTE:不要使用閘道器來發送封包,只發送到直接聯網的主機。這個標誌主要用於診斷或者路由程式。

  • MSG_DONTWAIT:操作不會被阻塞。

  • MSG_EOR:終止一個記錄。

  • MSG_MORE:呼叫者有更多的資料需要傳送。

  • MSG_NOSIGNAL:當另一端終止連線時,請求在基於流的錯誤套接字上不要傳送SIGPIPE訊號。

  • MSG_OOB:傳送out-of-band資料(需要優先處理的資料),同時現行協議必須支援此種操作。

    傳輸層協議使用帶外資料(out-of-band,OOB)來發送一些重要的資料,如果通訊一方有重要的資料需要通知對方時,協議能夠將這些資料快速地傳送到對方。

    為了傳送這些資料,協議一般不使用與普通資料相同的通道,而是使用另外的通道。linux系統的套接字機制支援低層協議傳送和接受帶外資料。但是TCP協議沒有真正意義上的帶外資料。為了傳送重要協議,TCP提供了一種稱為緊急模式(urgent mode)的機制。TCP協議在資料段中設定URG位,表示進入緊急模式。接收方可以對緊急模式採取特殊的處理。很容易看出來,這種方式資料不容易被阻塞,並且可以通過在我們的伺服器端程式裡面捕捉SIGURG訊號來及時接受資料。

dest_addr:目的地址,可為空

addrlen:地址屬性的長度(dest_addr的大小,其空則為0)

msg:(sendmsg、recvmsg函式使用) 有關內容請參考《socket程式設計:recvmsg 和 sendmsg 函式》

recvfrom()

recvfrom 會返回傳送端的地址,這樣對伺服器來說,由於時 UDP socket 物件沒有記錄對應的IP和埠資訊(記錄也沒有用,UDP不穩定,隨時可能變化),會需要用到該地址給客戶端來發送響應。對於客戶端,由於每次始終是知道伺服器IP地址和埠(和一個伺服器互動),所以無需記錄(除非UDP客戶端需要和多個伺服器互動,需要一一記錄,才能確保互動正確)

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

ssize_trecv(int sockfd, void *buf, size_t len, int flags);

ssize_trecvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

ssize_trecvmsg(int sockfd, struct msghdr *msg, int flags);

描述:從一個套介面接收訊息

recvfromrecvmsg()用來從一個套介面接收訊息,也可以用來在一個面向連線或非連線的套介面上接收資料。

recv呼叫通常只用於已建立連線的套介面,等效於引數src_addr為NULL的recvfrom呼叫。

close() 與 shutdown()

c
#include<unistd.h>

intclose(int fd);


#include<sys/socket.h>

intshutdown(int sockfd, int how);

close 可以用來關閉一個socket。

Linux的close函式和Windows的closesocket函式意味著完全斷開連線,完全斷開不僅指無法傳輸資料,而且也不能接收資料。在某些情況下,通訊一方呼叫close或closesocket函式斷開連線就顯得不太優雅。 ( 只要對方能收到FD_CLOSE,而不是傻等在迴圈裡,應該都算優雅吧。)

本端程式想傳送的資料都已經投放到傳送緩衝區內了,等待對方收到資料後,對方主動關閉連線,本方檢測到對方套接字關閉事件後,被動的關閉自己的連線。
1.註冊FD_CLOSE的通知。
2.把所有要傳送的資料都發送完畢後,呼叫shutdown(s, SD_SEND)。
3.sleep一會,或者阻塞呼叫recv知道他返回0或socket_error,然後呼叫closesocket。

但是,網上還有很多資料說,呼叫setsockopt設定so_linger選項時,如果設定了SO_DONTLINGER,或者SO_LINGER並且間隔非0,也屬於優雅的關閉。這種情況下,tcp在呼叫closesocket時,也會在關閉套接字資源之前,盡力的將傳送緩衝區內的資料傳送出去。看了一下MSDN,基本上也是這麼解釋的。

為了解決這類問題,“只關閉一部分資料交換中使用的流”的方法應運而生。斷開一部分連線是指,可以傳輸資料但無法接收,或可以接收資料但無法傳輸。即只關閉流的一半。

引數解析:

sock:需要斷開的套接字檔案描述符。

how:傳遞斷開方式資訊

  • SHUT_RD:斷開輸入流。
  • SHUT_WR:斷開輸出流。
  • SHUT_RDWR:同時斷開I/O流。(不允許進一步接收和傳輸)

返回值:成功返回0,失敗返回-1。

例程

我們在2個非親緣程序中實現一收一發的通訊。要求先啟動伺服器程式,客戶端隨後啟動。

client.c

c
/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  client.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

intmain(int argc, char *argv[]){
    int my_socket;
    unsigned int len;

    // 建立套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 定義傳送的訊息
    info buf ={0};
    sprintf(buf.name, "schips");
    sprintf(buf.text, "Socket send");

    // 指定地址
    structsockaddr_inaddr = {0};
    addr.sin_family = AF_INET;  // 地址協議族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定埠號

    // 傳送訊息
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 接收伺服器的訊息
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 關閉連線
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}

server.c

c
/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  server.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

intmain(int argc, char *argv[]){
    int my_socket;
    unsigned int len;

    // 建立套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);


    // 用於接收訊息
    info buf ={0};

    // 指定地址
    structsockaddr_inaddr = {0};
    addr.sin_family = AF_INET;  // 地址協議族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定埠號

    // 伺服器 繫結
    bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));

    // 接收並列印訊息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 回覆訊息
    sprintf(buf.name, "Server");
    sprintf(buf.text, "Had recvied your message");
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 關閉連線
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}

知識

UDP:User Datagram Protocol的縮寫。
UDP不提供複雜控制機制,利用IP提供面向無連線的通訊服務。且它是將應用程式發來的資料在收到的那一刻,立即按照原樣傳送到網路上的一種機制。

UDP面向無連線,可以隨時傳送資料。它常用於幾個方面:

  • 包總量較少的通訊(DNS、SNMP等)
  • 視訊、音訊等多媒體通訊(即時通訊)
  • 限定於LAN等特定網路中的應用通訊
  • 廣播通訊(廣播、多播)

典型的 UDP 通訊流程圖如下:

ServerClient雙方都建立socket物件socket()socket()伺服器一般繫結埠號bind()收發訊息sendto()/recvfrom()sendto()/recvfrom()關閉連線close()close()ServerClient

有關函式介紹

根據流程圖,我們知道,在UDP通訊中,使用到了這些函式:socket()bind()sendto()recvfrom()

socket

c
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>

intsocket(int domain, int type, int protocol);

描述:建立一個用於網路通訊的套接字。
引數解析

domain:

  • AF_INET:IPv4協議
  • AF_INET6:IPv6協議
  • AF_LOCAL:Unix域協議
  • AF_ROUTE:路由套介面
  • AF_KEY:金鑰套介面

在早期版本中的domain引數中還有以 PF_開頭的定義,當初設計者設計的時候,設想地址使用一套域,協議使用一套域,然後就沒有然後了。所以,實際上,PF_ 其實相當於 AF_

type:

  • SOCK_STREAM:提供面向連線的雙向可靠資料流,對應TCP協議

  • SOCK_DGRAM:提供無保障的面向訊息的雙向不可靠資料報,對應UDP,可用於在網路上發廣播資訊。(UDP)

  • SOCK_RAW:提供傳輸層以下的協議,可以訪問內部網路介面,例如接收和傳送ICMP報文

  • SOCK_RDM:提供可靠的資料報文,但可能資料會有亂序

  • SOCK_SEQPACKET:序列化包,提供一個序列化的、可靠的、雙向的基本連線的資料傳輸通道,資料長度定常。每次呼叫讀系統呼叫時資料需要將全部資料讀出

  • 同時,可以 與下面的值相或

    • SOCK_NONBLOCK . 非阻塞,也可以使用fcntl()來做這個事情
    • SOCK_CLOEXEC 在執行 exec 時關閉該描述符,相當於 FD_CLOEXEC

protocol:當type為SOCK_RAW時需要設定此值說明協議型別,其他型別設定為0即可, 對於protocol為0(IPPROTO_IP)的raw socket。用於接收任何的IP資料包。其中的校驗和和協議分析由程式自己完成

返回值:成功返回socket描述符;失敗返回-1,置errno:

底層協議模組可能會產生其他錯誤。

  • ERRORS:建立指定型別和/或pro-tocol的套接字的許可權被拒絕
  • EAFNOSUPPORT:不支援指定的地址族
  • EINVAL 未知協議,或協議系列不可用
  • EINVAL :無效的type
  • EMFILE :已達到每個程序對開啟的檔案描述符數的限制
  • ENFILE :已達到系統範圍內開啟檔案總數的限制
  • ENOBUFS or ENOMEM :可用記憶體不足。只有釋放足夠的資源,才能建立套接字
  • EPROTONOSUPPORT :此域中不支援協議型別或指定的協議

bind

一個套接字只是使用者程式與核心互動資訊的樞紐,它自身沒有太多的資訊,也沒有網路協議地址和埠號等資訊。進行網路通訊時,必須把一個套接字與一個地址相關聯,這個過程就是地址繫結的過程。
許多時候核心會我們自動繫結一個地址,然而有時使用者可能需要自己來完成這個繫結的過程,以滿足實際應用的需要。

最典型的情況是一個伺服器程序需要繫結一個眾所周知的地址或埠以等待客戶來連線。這個事由bind的函式完成。

伺服器啟動時需要繫結指定的埠來提供服務(以便於客戶向指定的埠傳送請求),對於伺服器socket繫結地址,一般而言將IP地址賦值為INADDR_ANY(該巨集值為0),即無論傳送到系統中的哪個IP地址(當伺服器有多張網絡卡時會有多個IP地址)的請求都採用該socket來處理,而無需指定固定IP。

對於Client,一般而言無需主動呼叫bind(),一切由作業系統來完成。在傳送資料前,作業系統會為套接字隨機分配一個可用的埠,同時將該套接字和本地地址資訊繫結。

一個網路應用程式只能繫結一個埠( 一個套接字只能 繫結一個埠 )。如果需要繫結多個埠,需要使用埠複用(略)。

c
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>

intbind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

/* 可能會用到以下結構體 */
structsockaddr {
   sa_family_t sa_family; // 地址族
   char        sa_data[14]; // 資料
}

/* 一般使用以下結構體代替 */
#include<netinet/in.h>
#include<arpa/inet.h>
structsockaddr_in {
    sa_family_t 	sa_family;	 // 地址族
	uint16_t 		sin_port; 	 // 16位 TCP/UDP 埠號
    structin_addrsin_addr;	 // 32位IP地址,裡面的內容是 In_addr_t
    char			sin_zero[8]; // 不使用
}

描述:將addr指向的sockaddr結構體中描述的一些屬性(IP地址、埠號、地址簇)與socket套接字繫結(也叫給套接字命名)。 為socket套接字關聯了一個相應的地址與埠號,即傳送到地址值該埠的資料可通過socket讀取和使用。

引數解析

sockfd:由socket()函式成功返回的結果

addr: 設定有網路屬性的sockaddr_in或sockaddr 結構體

  • sa_family

addrlen:sockaddr_in或sockaddr 結構體的大小

與 sockaddr_in 有關的函式

常在埠號中使用的:

c
#include<arpa/inet.h>

// 主機位元組序到網路位元組序
u_long htonl(u_long hostlong);
u_short htons(u_short short);

// 網路位元組序到主機位元組序
u_long ntohl(u_long hostlong);
u_short ntohs(u_short short);

常在地址轉換中使用的:

c
#include<arpa/inet.h>

// 將網路地址轉換為 以 點分十進位制表示的字串(字串存在於靜態記憶體中)
char *inet_ntoa(struct in_addr);

// 點分十進位制字串 轉 32位(IPV4 IP)二進位制
in_addr_tinet_addr(constchar* cp);

範例:

c
	...
    structsockaddr_inaddr ={0};
    addr.sin_family = AF_INET; 	  // 地址協議族 
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
    addr.sin_port = htons(12345); //指定埠號
	...

sendto()

關於傳送,有下列這些函式。

send只可用於基於連線(TCP)的套接字,send 和 write唯一的不同點是標誌的存在,當標誌為0時,send等同於write。

sendto 和 sendmsg既可用於無連線的套接字,也可用於基於連線的套接字。除了套接字設定為非阻塞模式,呼叫將會阻塞直到資料被髮送完。

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

ssize_tsend(int sockfd, constvoid *buf, size_t len, int flags);

ssize_tsendto(int sockfd, constvoid *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

ssize_tsendmsg(int sockfd, const struct msghdr *msg, int flags);

/* 可能用到的結構體 */
structmsghdr {
   void         *msg_name;       /* optional address */ // 用於無連線套接字的場合
   socklen_t     msg_namelen;    /* size of address */  // 用於無連線套接字的場合
   structiovec *msg_iov;        /* scatter/gather array */
   size_t        msg_iovlen;     /* # elements in msg_iov */
   void         *msg_control;    /* ancillary data, see below */
   size_t        msg_controllen; /* ancillary data buffer len */
   int           msg_flags;      /* flags (unused) */
};

描述:傳送資料到指定的地址

引數解析:

sockfd:要傳送到哪個套接字

buf:要傳送資料首地址

len:要傳送的資料長度

flags:是以下零個或者多個標誌的相或結果

  • MSG_DONTROUTE:不要使用閘道器來發送封包,只發送到直接聯網的主機。這個標誌主要用於診斷或者路由程式。

  • MSG_DONTWAIT:操作不會被阻塞。

  • MSG_EOR:終止一個記錄。

  • MSG_MORE:呼叫者有更多的資料需要傳送。

  • MSG_NOSIGNAL:當另一端終止連線時,請求在基於流的錯誤套接字上不要傳送SIGPIPE訊號。

  • MSG_OOB:傳送out-of-band資料(需要優先處理的資料),同時現行協議必須支援此種操作。

    傳輸層協議使用帶外資料(out-of-band,OOB)來發送一些重要的資料,如果通訊一方有重要的資料需要通知對方時,協議能夠將這些資料快速地傳送到對方。

    為了傳送這些資料,協議一般不使用與普通資料相同的通道,而是使用另外的通道。linux系統的套接字機制支援低層協議傳送和接受帶外資料。但是TCP協議沒有真正意義上的帶外資料。為了傳送重要協議,TCP提供了一種稱為緊急模式(urgent mode)的機制。TCP協議在資料段中設定URG位,表示進入緊急模式。接收方可以對緊急模式採取特殊的處理。很容易看出來,這種方式資料不容易被阻塞,並且可以通過在我們的伺服器端程式裡面捕捉SIGURG訊號來及時接受資料。

dest_addr:目的地址,可為空

addrlen:地址屬性的長度(dest_addr的大小,其空則為0)

msg:(sendmsg、recvmsg函式使用) 有關內容請參考《socket程式設計:recvmsg 和 sendmsg 函式》

recvfrom()

recvfrom 會返回傳送端的地址,這樣對伺服器來說,由於時 UDP socket 物件沒有記錄對應的IP和埠資訊(記錄也沒有用,UDP不穩定,隨時可能變化),會需要用到該地址給客戶端來發送響應。對於客戶端,由於每次始終是知道伺服器IP地址和埠(和一個伺服器互動),所以無需記錄(除非UDP客戶端需要和多個伺服器互動,需要一一記錄,才能確保互動正確)

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

ssize_trecv(int sockfd, void *buf, size_t len, int flags);

ssize_trecvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

ssize_trecvmsg(int sockfd, struct msghdr *msg, int flags);

描述:從一個套介面接收訊息

recvfromrecvmsg()用來從一個套介面接收訊息,也可以用來在一個面向連線或非連線的套介面上接收資料。

recv呼叫通常只用於已建立連線的套介面,等效於引數src_addr為NULL的recvfrom呼叫。

close() 與 shutdown()

c
#include<unistd.h>

intclose(int fd);


#include<sys/socket.h>

intshutdown(int sockfd, int how);

close 可以用來關閉一個socket。

Linux的close函式和Windows的closesocket函式意味著完全斷開連線,完全斷開不僅指無法傳輸資料,而且也不能接收資料。在某些情況下,通訊一方呼叫close或closesocket函式斷開連線就顯得不太優雅。 ( 只要對方能收到FD_CLOSE,而不是傻等在迴圈裡,應該都算優雅吧。)

本端程式想傳送的資料都已經投放到傳送緩衝區內了,等待對方收到資料後,對方主動關閉連線,本方檢測到對方套接字關閉事件後,被動的關閉自己的連線。
1.註冊FD_CLOSE的通知。
2.把所有要傳送的資料都發送完畢後,呼叫shutdown(s, SD_SEND)。
3.sleep一會,或者阻塞呼叫recv知道他返回0或socket_error,然後呼叫closesocket。

但是,網上還有很多資料說,呼叫setsockopt設定so_linger選項時,如果設定了SO_DONTLINGER,或者SO_LINGER並且間隔非0,也屬於優雅的關閉。這種情況下,tcp在呼叫closesocket時,也會在關閉套接字資源之前,盡力的將傳送緩衝區內的資料傳送出去。看了一下MSDN,基本上也是這麼解釋的。

為了解決這類問題,“只關閉一部分資料交換中使用的流”的方法應運而生。斷開一部分連線是指,可以傳輸資料但無法接收,或可以接收資料但無法傳輸。即只關閉流的一半。

引數解析:

sock:需要斷開的套接字檔案描述符。

how:傳遞斷開方式資訊

  • SHUT_RD:斷開輸入流。
  • SHUT_WR:斷開輸出流。
  • SHUT_RDWR:同時斷開I/O流。(不允許進一步接收和傳輸)

返回值:成功返回0,失敗返回-1。

例程

我們在2個非親緣程序中實現一收一發的通訊。要求先啟動伺服器程式,客戶端隨後啟動。

client.c

c
/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  client.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

intmain(int argc, char *argv[]){
    int my_socket;
    unsigned int len;

    // 建立套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);

    // 定義傳送的訊息
    info buf ={0};
    sprintf(buf.name, "schips");
    sprintf(buf.text, "Socket send");

    // 指定地址
    structsockaddr_inaddr = {0};
    addr.sin_family = AF_INET;  // 地址協議族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定埠號

    // 傳送訊息
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 接收伺服器的訊息
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 關閉連線
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}

server.c

c
/*
#    Copyright By Schips, All Rights Reserved
#    https://gitee.com/schips/
#
#    File Name:  server.c
#    Created  :  Sat 21 Mar 2020 04:43:39 PM CST
*/

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<sys/types.h>          /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

typedef struct _info {
    char name[10];
    char text[54];
}info;

intmain(int argc, char *argv[]){
    int my_socket;
    unsigned int len;

    // 建立套接字
    my_socket = socket(AF_INET, SOCK_DGRAM, 0);// IPV4, UDP socket
    if(my_socket == -1) { perror("Socket"); }
    printf("Creat a socket :[%d]\n", my_socket);


    // 用於接收訊息
    info buf ={0};

    // 指定地址
    structsockaddr_inaddr = {0};
    addr.sin_family = AF_INET;  // 地址協議族
        addr.sin_addr.s_addr = inet_addr("127.0.0.1");   //指定 IP地址
        addr.sin_port = htons(12345); //指定埠號

    // 伺服器 繫結
    bind(my_socket, (struct sockaddr *)&addr, sizeof(addr));

    // 接收並列印訊息
    //recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
    recvfrom(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, &len);
        perror("recvfrom");

    printf("%s\n", buf.name);
    printf("%s\n", buf.text);

    // 回覆訊息
    sprintf(buf.name, "Server");
    sprintf(buf.text, "Had recvied your message");
    sendto(my_socket, &buf, sizeof(buf), 0, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
        perror("sendto");

    // 關閉連線
    //shutdown(my_socket, SHUT_RDWR); perror("shutdown");
    //printf("%d\n", errno);
    return close(my_socket); perror("close");
    printf("%d\n", errno);
    return errno;
}