天天看點

《網絡程式設計 —— socket程式設計執行個體》

一、socket簡介

1、網絡中程序間通信

本機程序使用程序号差別不同的程序程序間通信方式有管道、信号、消息隊列、共享記憶體、信号量等。網絡中程序間的通信首先需要識别程序所在主機在網絡中的唯一辨別即網絡層的ip位址主機上的程序可以通過傳輸層的協定與端口号識别。

2、socket原理

    socket是應用層與tcp/ip協定族通信的中間軟體抽象層是一種程式設計接口。socket屏蔽了不同網絡協定的差異支援面向連接配接(transmission control protocol - tcpip)和無連接配接(user datagram protocol-udp 和 inter-network packet exchange-ipx)的傳輸協定。

《網絡程式設計 —— socket程式設計執行個體》

二、socket通信的基礎知識

1、網絡位元組序

主機位元組序即記憶體中存儲位元組的方式分為大端序和小端序。何為大端、小端呢小端将低位元組存儲在低位址。大端将高位元組存儲在低位元組。網絡中在處理多位元組順序時一般采用大端序。在網絡傳輸時需要把主機位元組序轉換到網絡位元組序常用的轉換函數如下

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

2、資料結構

struct sockaddr {

    sa_family_t sa_family; /* address family, af_xxx */

    char sa_data[14]; /* 14 bytes of protocol address */

};

struct sockaddr_in {

    __kernel_sa_family_t sin_family; /* address family */

    __be16 sin_port; /* port number */

    struct in_addr sin_addr; /* internet address */

    /* pad to size of `struct sockaddr'. */

    unsigned char __pad[__sock_size__ - sizeof(short int) -

sizeof(unsigned short int) - sizeof(struct in_addr)];

/* internet address. */

struct in_addr {

    __be32 s_addr;

3、ip位址轉換

int inet_aton(const char *cp, struct in_addr *inp);

    将cp所指的字元串ip位址轉換成32位的網絡位元組序ip位址

in_addr_t inet_addr(const char *cp);

    将cp所指的字元串ip位址轉換成32位的網絡位元組序ip位址傳回

char *inet_ntoa(struct in_addr in);

    将32位網絡位元組序ip位址轉換成點分十進制的字元串ip位址

4、位址結構使用

a、定義一個struct sockaddr_in類型的變量并清空

        struct sockaddr_in serveraddr;

    bzero(&serveraddr,  sizeof(serveraddr));

b、填充位址資訊

    serveraddr.sin_family = af_inet;

    serveraddr.sin_port = htons(8080);

    serveraddr.sin_addr.s_addr = inet_addr("192.168.6.100");

c、将該變量強制轉換為struct sockaddr類型在函數中使用

        bind(listenfd, (struct sockaddr *)&serveraddr,  sizeof(serveraddr));

5、tcp連接配接的建立

    tcp協定通過三個封包段完成連接配接的建立建立連接配接的過程稱為三次握手(three-way handshake)。

        第一次握手建立連接配接時用戶端發送syn包(syn=j)到伺服器并進入syn_send狀态等待伺服器确認syn同步序列編号(synchronize sequence numbers)。

    第二次握手伺服器收到syn包必須确認客戶的synack=j+1同時自己也發送一個syn包syn=k即syn+ack包此時伺服器進入syn_recv狀态

第三次握手用戶端收到伺服器的syn+ack包向伺服器發送确認包ack(ack=k+1)此包發送完畢用戶端和伺服器進入established狀态完成三次握手。

    一個完整的三次握手是 請求---應答---再次确認。

《網絡程式設計 —— socket程式設計執行個體》

當用戶端調用connect時觸發了連接配接請求向伺服器發送了syn j包這時connect進入阻塞狀态伺服器監聽到連接配接請求即收到syn j包調用accept函數接收請求向用戶端發送syn k ack j+1這時accept進入阻塞狀态用戶端收到伺服器的syn k ack j+1之後這時connect傳回并對syn k進行确認伺服器收到ack k+1時accept傳回至此三次握手完畢連接配接建立。

6、tcp連接配接的斷開

    終止一個連接配接要經過四次握手簡稱四次握手釋放

《網絡程式設計 —— socket程式設計執行個體》

tcp連接配接是全雙工的每個方向都必須單獨進行關閉。當一方完成資料發送任務後就能發送一個fin來終止這個方向的連接配接。收到一個 fin隻意味着這一方向上沒有資料流動一個tcp連接配接在收到一個fin後仍能發送資料。首先進行關閉的一方将執行主動關閉而另一方執行被動關閉。

a、用戶端a發送一個fin用來關閉客戶a到伺服器b的資料傳送

b、伺服器b收到這個fin它發回一個ack确認序号為收到的序号加1。和syn一樣一個fin将占用一個序号。

   c、伺服器b關閉與用戶端a的連接配接發送一個fin給用戶端a。

   d、用戶端a發回ack封包确認并将确認序号設定為收到序号加1。

    為什麼建立連接配接協定是三次握手而關閉連接配接卻是四次握手呢

     因為服務端的listen狀态下的socket當收到syn封包的建連請求後它可以把ack和synack起應答作用而syn起同步作用放在一個封包裡來發送。但關閉連接配接時當收到對方的fin封包通知時僅僅表示對方沒有資料發送給你了但你所有的資料未必都全部發送給對方了你未必會馬上會關閉socket你可能還需要發送一些資料給對方之後再發送fin封包給對方來表示你同意現在可以關閉連接配接了是以ack封包和fin封包多數情況下都是分開發送的。

    為什麼time_wait狀态還需要等2msl後才能傳回到closed狀态

        雖然雙方都同意關閉連接配接了而且握手的4個封包也都協調和發送完畢按理可以直接回到closed狀态就好比從syn_send狀态到 establish狀态那樣但是因為我們必須要假想網絡是不可靠的你無法保證你最後發送的ack封包會一定被對方收到是以對方處于 last_ack狀态下的socket可能會因為逾時未收到ack封包而重發fin封包是以這個time_wait狀态的作用就是用來重發可能丢失的 ack封包。

7、getaddrinfo

int getaddrinfo(const char *node, const char *service,

                       const struct addrinfo *hints,

                       struct addrinfo **res);

    node:一個主機名域名或者位址串(ipv4點分十進制串或者ipv6的16進制串)

        service服務名可以是十進制的端口号可以是已定義服務名如ftp、http等

        hints可以是一個空指針也可以是一個指向某個 addrinfo結構體的指針調用者在這個結構中填入關于期望傳回的資訊類型的暗示。舉例來說如果指定的服務既支援tcp也支援udp那麼調用者可以把hints結構中的ai_socktype成員設定成sock_dgram使得傳回的僅僅是适用于資料報套接口的資訊。

        result本函數通過result指針參數傳回一個指向addrinfo結構體連結清單的指針。

        傳回值0——成功非0——出錯

struct addrinfo {

               int              ai_flags;

               int              ai_family;//af_inet,af_inet6或者af_unspec

               int              ai_socktype;//sock_stream or sock_dgram

               int              ai_protocol;//0

               size_t           ai_addrlen;

               struct sockaddr *ai_addr;

               char            *ai_canonname;

               struct addrinfo *ai_next;

           };

ai_flags:

    ai_passive套接字位址用于監聽綁定

    ai_canonname需要一個規範名而不是别名

    ai_v4mapped如果沒有找到ipv6位址傳回映射到ipv6格式的ipv4位址

    ai_addrconfig查詢配置的位址類型ipv4或ipv6

    ai_numericserv以端口号傳回服務

    ai_numerichost以數字格式傳回主機位址

   gethostbyname函數僅支援ipv4

struct hostent *gethostbyname(const char *name);

struct hostent {

               char  *h_name;            /* official name of host */

               char **h_aliases;         /* alias list */

               int    h_addrtype;        /* host address type */

               int    h_length;          /* length of address */

               char **h_addr_list;       /* list of addresses */

           }

           #define h_addr h_addr_list[0] /* for backward compatibility */

    name主機名或域名

三、socket接口函數

socket程式設計的一般流程如下

《網絡程式設計 —— socket程式設計執行個體》

1、socket

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

    建立一個socket

    domain即協定域又稱為協定族family。常用的協定族有af_inet、af_inet6、af_local或稱af_unixunix域socket、af_route等等。協定族決定了socket的位址類型在通信中必須采用對應的位址如af_inet決定了要用ipv4位址32位的與端口号16位的的組合、af_unix決定了要用一個絕對路徑名作為位址。

    type指定socket類型。常用的socket類型有sock_stream、sock_dgram、sock_raw、sock_packet、sock_seqpacket等等。

    protocol指定協定。常用的協定有ipproto_tcp、ipptoto_udp、ipproto_sctp、ipproto_tipc等它們分别對應tcp傳輸協定、udp傳輸協定、stcp傳輸協定、tipc傳輸協定。當protocol為0時會自動選擇type類型對應的預設協定。

2、bind

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

        把一個位址族中的特定位址賦給socket

        sockfd即socket描述字通過socket函數建立得到唯一辨別一個socket。

        addrconst struct sockaddr *指針指向要綁定給sockfd的協定位址。

ipv4的協定位址結構

    sa_family_t    sin_family; /* address family: af_inet */

    in_port_t      sin_port;   /* port in network byte order */

    struct in_addr sin_addr;   /* internet address */

/* internet address. */struct in_addr {

    uint32_t       s_addr;     /* address in network byte order */

ipv6的協定位址結構

struct sockaddr_in6 {

    sa_family_t     sin6_family;   /* af_inet6 */

    in_port_t       sin6_port;     /* port number */

    uint32_t        sin6_flowinfo; /* ipv6 flow information */

    struct in6_addr sin6_addr;     /* ipv6 address */

    uint32_t        sin6_scope_id; /* scope id (new in 2.4) */

struct in6_addr {

    unsigned char   s6_addr[16];   /* ipv6 address */

3、listen

int listen(int sockfd, int backlog);

    設定sockfd套接字為監聽套接字

    sockfd參數即為要監聽的socket描述字

    backlog參數為相應socket可以排隊的最大連接配接個數。

        socket函數建立的socket預設是一個主動類型的listen函數将socket變為被動類型的等待客戶的連接配接請求。

4、accept

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

接收用戶端的請求建立連接配接套接字

    參數sockfd是監聽套接字用來監聽一個端口

    參數addr用來接收用戶端的協定位址可以設定為null。

    參數len表示接收的用戶端的協定位址addr結構的大小的可設定為null

    如果accept成功傳回則伺服器與客戶已經正确建立連接配接了伺服器通過accept傳回的套接字來完成與客戶的通信。

    accept預設會阻塞程序直到有一個客戶連接配接建立後傳回傳回的是一個新可用的連接配接套接字。

    監聽套接字: 在調用listen函數之後socket函數生成的主動連接配接的普通套接字就轉變為監聽套接字一般被accept函數調用的sockfd就是監聽套接字

    連接配接套接字accept函數傳回的是連接配接套接字代表與用戶端已經建立連接配接

     一個伺服器程式通常隻建立一個監聽套接字在伺服器程式的生命周期内一直存在。核心為每個由伺服器程序接受的客戶連接配接建立了一個連接配接套接字當伺服器完成了對某個客戶的服務相應的連接配接套接字就被關閉。

     連接配接套接字并沒有占用新的端口與用戶端通信依然使用的是與監聽套接字sockfd一樣的端口号。

5、connect

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

sockfd參數即為用戶端的socket描述字

addr參數為伺服器的socket位址

addrlen參數為socket位址的長度

成功執行後用戶端通過調用connect函數來建立與tcp伺服器的連接配接。

6、資料傳輸操作

ssize_t read(int fd, void *buf, size_t count);

        read函數是負責從連接配接套接字fd中讀取内容。

    fd參數是accept函數建立的連接配接套接字

    buf參數是讀取的内容存放的記憶體緩沖區

    count參數是要讀取的内容的大小

    當讀成功時read傳回實際所讀的位元組數如果傳回的值是0表示已經讀到檔案的結束小于0表示出現了錯誤。如果錯誤為eintr說明讀是由中斷引起的如果是econnrest表示網絡連接配接出了問題。

ssize_t write(int fd, const void *buf, size_t count);

    write函數是向連接配接套接字fd寫入内容

    fd參數表示建立的連接配接套接字

    buf參數表示要寫入内容所在的記憶體緩沖區

    count參數表示要寫入的内容的大小

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

    send函數向連接配接套接字sockfd發送内容

    sockfd參數表示發送到的連接配接套接字

    buf參數表示要發送的内容所在的記憶體緩沖區

    len參數表示要發送内容的長度

    flags參數表示send的辨別符一般為0

    成功傳回實際發送的位元組數出錯傳回-1

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

    recv函數從連接配接套接字sockfd接收内容

    sockfd參數表示從哪個連接配接套接字接收内容

    buf參數表示接收的内容存放的記憶體緩沖區

    len參數表示接收内容的實際位元組數

    flags參數表示recv操作辨別符一般為0

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

    sendmsg函數向連接配接套接字sockfd發送資訊

    sockfd參數表示向哪個連接配接套接字發送資訊

    msg參數表示要發送的資訊的記憶體緩沖區

    flags參數表示sendmsg函數操作的辨別一般為0msg_dontwait表示非阻塞模式msg_oob表示發送帶外資料

struct msghdr {

               void         *msg_name;       /* optional address */

               socklen_t     msg_namelen;    /* size of address */

               struct iovec *msg_iov;        /* scatter/gather array */

               size_t        msg_iovlen;     /* # elements in msg_iov */

               void         *msg_control;    /* ancillary data, see below */

               socklen_t     msg_controllen; /* ancillary data buffer len */

               int           msg_flags;      /* flags on received message */

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

    recvmsg函數從連接配接套接字scokfd接收資訊

    sockfd參數表示從哪個連接配接套接字接收資訊

    msg參數表示接收的資訊存放的記憶體緩沖區

    flags參數表示recvmsg函數操作的辨別符

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,

                      const struct sockaddr *dest_addr, socklen_t addrlen);

    sendto函數表示向連接配接套接字發送内容

    buf參數表示發送的内容所在的記憶體緩沖區

    len參數表示發送的資訊的位元組數

    flags參數表示sendto函數的操作辨別符

    dest_addr參數表示發送到的位址的指針

    addrlen參數表示發送到的位址的長度

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,

                        struct sockaddr *src_addr, socklen_t *addrlen);

    recvfrom函數從連接配接套接字sockfd接收資訊

    buf參數表示接收的資訊存放的記憶體緩沖區

    len參數表示接收的實際位元組數

    flags參數表示recvfrom函數的操作辨別符

    src_addr參數表示接收的資訊來自的主機協定位址所存放的記憶體緩沖區

    addrlen參數表示接收資訊的源主機協定位址的長度

7、close

int close(int fd);

    關閉斷開連接配接套接字

    fd參數表示要斷開的連接配接套接字

四、程式執行個體

服務端server.c

用戶端client.c