網絡IPC:套接字

網絡進程間通訊:socket API簡介

不一樣計算機(經過網絡相連)上運行的進程相互通訊機制稱爲網絡進程間通訊(network IPC)。編程

在本地能夠經過進程PID來惟一標識一個進程,可是在網絡中這是行不通的。其實TCP/IP協議族已經幫咱們解決了這個問題,網絡層的「ip地址」能夠惟一標識網絡中的主機,而傳輸層的「協議+端口」能夠惟一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)構成套接字,就能夠標識網絡的進程了,網絡中的進程通訊就能夠利用這個標誌與其它進程進行交互。服務器

套接字是通訊端口的抽象!經過套接字網絡IPC接口,進程可以使用該接口和其餘進程通訊。網絡

幾個定義:dom

  1. IP地址:即依照TCP/IP協議分配給本地主機的網絡地址,兩個進程要通信,任一進程首先要知道通信對方的位置,即對方的IP。
  2. 端口號:用來辨別本地通信進程,一個本地的進程在通信時均會佔用一個端口號,不一樣的進程端口號不一樣,所以在通信前必需要分配一個沒有被訪問的端口號。
  3. 鏈接:指兩個進程間的通信鏈路。
  4. 半相關:網絡中用一個三元組能夠在全局惟一標誌一個進程:(協議,本地地址,本地端口號)這樣一個三元組,叫作一個半相關,它指定鏈接的每半部分。
  5. 全相關:一個完整的網間進程通訊須要由兩個進程組成,而且只能使用同一種高層協議。也就是說,不可能通訊的一端用TCP協議,而另外一端用UDP協議。所以一個完整的網間通訊須要一個五元組來標識:(協議,本地地址,本地端口號,遠地地址,遠地端口號),這樣一個五元組,叫作一個相關(association),即兩個協議相同的半相關才能組合成一個合適的相關,或徹底指定組成一鏈接。

套接字描述符

套接字是端點的抽象。與應用進程要使用文件描述符訪問文件同樣,訪問套接字也須要用套接字描述符。套接字描述符在UNIX系統中是用文件描述符實現的。socket

要建立一個套接字,能夠調用socket函數。函數

#include<sys/socket.h>
int socket(int domain, int type, int protocol);

  參數:ui

做用:socket()用於建立一個socket描述符(socket descriptor),它惟一標識一個socket。spa

網絡字節序

網絡協議指定了字節序,所以異構計算機系統可以交換協議信息而不會混淆字節序。TCP/IP協議棧採用大端字節序。應用進程交換格式化數據時,字節序問題就會出現。對於TCP/IP,地址用網絡字節序來表示,因此應用進程有時須要在處理器的字節序與網絡字節序之間轉換。.net

#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);

這些函數名很好記,h表示host,n表示network, l表示32位長整數,s表示16位短整數指針

在將一個地址綁定到socket的時候,請先將主機字節序轉換成爲網絡字節序,對主機字節序不要作任何假定,務必將其轉化爲網絡字節序再賦給socket!

將套接字與地址綁定

與客戶端的套接字關聯的地址意義不大,可讓系統選擇一個默認的地址。然而,對於服務器,須要給一個接收客戶端請求的套接字綁定一個衆所周知的地址。客戶端應有一種方法用以鏈接服務器的地址,最簡單的方法就是爲服務器保留一個地址而且在/etc/services或某個名字服務(name service)中註冊。

  能夠用bind函數來搞定這個問題:

#include <sys/types.h>        
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數:

第一個參數:bind()函數把一個地址族中的特定地址賦給該sockfd(套接字描述字)。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。 

第二個參數:struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不一樣而不一樣:

地址格式

地址標識了特定通訊域中的套接字端點,地址格式與特定的通訊域相關。爲使不一樣格式地址可以被傳入到套接字函數,地址需被強轉爲通用的地址結構sockaddr表示。

//頭文件
#include<netinet/in.h>

struct sockaddr 是一個通用地址結構,該結構定義以下: 

struct sockaddr
{
   sa_family_t sa_family;
   char        sa_data[14];
}

IPV4因特網域:

//ipv4對應的是: 
/* 網絡地址 */
struct in_addr 
{
    uint32_t       s_addr;     /* address in network byte order */
};

struct sockaddr_in {
    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 */
};

IPv6因特網域:

//ipv6對應的是: 
struct in6_addr 
{ 
    unsigned char   s6_addr[16];   /* IPv6 address */ 
};

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) */ 
};

  Unix域對應的是: 

#define UNIX_PATH_MAX    108

struct sockaddr_un 
{ 
    sa_family_t sun_family;               /* AF_UNIX */ 
    char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
};

第三個參數:addrlen 對應的是地址的長度

返回值:成功返回0,出錯返回-1

做用:將套接字與端口號綁定,即把一個ip地址和端口號組合賦給socket


點分十進制IP與網絡字節序IP之間的轉換

有時須要打印出能被人而不是計算機所理解的地址格式。咱們能夠利用函數來進行二進制地址格式與點分十進制格式的相互轉換。可是這些函數僅支持IPv4地址。

 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 //點分十進制IP轉換網絡字節序IP
 int inet_aton(const char *cp, struct in_addr *inp);
 //點分十進制IP轉換網絡字節序IP
 in_addr_t inet_addr(const char *cp);
 //網絡字節序IP 轉化點分十進制IP
 char *inet_ntoa(struct in_addr in);

其中inet_pton和inet_ntop不只能夠轉換IPv4的in_addr,還能夠轉換IPv6的in6_addr,所以函數接口是void* 類型!

#include <arpa/inet.h>
//網絡字節序IP 轉化點分十進制IP
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
//點分十進制IP轉換網絡字節序IP
int inet_pton(int af, const char *src, void *dst);

監聽

若是做爲一個服務器,在調用socket()、bind()以後就會調用listen()來監聽這個socket,若是客戶端這時調用connect()發出鏈接請求,服務器端就會接收到這個請求。

服務器調用 listen 來宣告能夠接收鏈接請求!

#include <sys/types.h>    
#include <sys/socket.h>
 int listen(int sockfd, int backlog);

參數:sockfd爲要監聽的socket描述字,backlog爲相應socket能夠排隊的最大鏈接個數  

返回值:成功返回0,出錯返回-1

做用:socket函數建立一個套接字時,默認是一個主動套接字,listen函數把一個未調用connect的未鏈接的套接字轉換成一個被動套接字,指示內核應接收指向該套接字的鏈接請求。(主動/客戶 -> 被動/服務器)


鏈接

若是是面向鏈接的網絡服務,在開始交換數據前,都要在請求服務的進程套接字(客戶端)和提供服務的進程套接字(服務器)之間創建一個鏈接,使用connect函數:

#include <sys/types.h>        
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

參數:第一個參數sockfd爲客戶端的socket描述字,第二參數爲服務器的socket地址,第三個參數爲socket地址的長度。 

返回值:成功返回0,出錯返回-1

做用:客戶端經過調用connect函數來創建與TCP服務器的鏈接

注意:在connect中所指定的地址是想與之通訊的服務器地址。若是sockfd沒有綁定到一個地址,connect會給調用者綁定一個默認地址!


使用accept函數得到鏈接請求並創建鏈接

#include <sys/types.h>          
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

參 數 :第一個參數爲服務器的socket描述字,第二個參數爲指向struct sockaddr *的指針,用於返回客戶端的協議地址,第三個參數爲協議地址的長度 

返回值:若是accpet成功,那麼其返回值是由內核自動生成的一個全新的描述字,該描述符鏈接到調用connect的客戶端。這個新的套接字描述符和原始的套接字描述符具備相同的套接字類型和地址族。

注 意:傳給accept的原始套接字沒有關聯到這個鏈接,而是繼續保持可用狀態並接受其它鏈接請求!

通俗點來講,accept的第一個參數爲服務器的socket描述字,是服務器開始調用socket()函數生成的,稱爲監聽socket描述字;而accept函數返回的是已鏈接的socket描述字。一個服務器一般一般僅僅只建立一個監聽socket描述字,它在該服務器的生命週期內一直存在。內核爲每一個由服務器進程接受的客戶鏈接建立了一個已鏈接socket描述字,當服務器完成了對某個客戶的服務,相應的已鏈接socket描述字就被關閉。


數據傳輸

既然套接字端點表示文件描述符,那麼只要創建鏈接,就可使用write和read來經過套接字通訊了。

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

write()會把指針buf所指的內存寫入count個字節到參數fd所指的文件內(文件讀寫位置也會隨之移動),若是順利write()會返回實際寫入的字節數。當有錯誤發生時則返回-1,錯誤代碼存入errno中!

read()會把參數fd所指的文件傳送nbyte個字節到buf指針所指的內存中,成功返回讀取的字節數,出錯返回-1並設置errno,若是在調read以前已到達文件末尾,則此次read返回0 。

若是想指定多個選項、從多個客戶端接收數據包或發送帶外數據,須要採用6個傳遞數據的套接字函數中的一個。

三個函數用來發送數據:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

 sendto()適用於已鏈接的數據報或流式套接口發送數據。

參數:

  •   sockfd:一個標識套接口的描述字。
  •   buf:包含待發送數據的緩衝區。
  •   len:buf緩衝區中數據的長度。
  •   flags:調用方式標誌位。
  •   dest_addr:(可選)指針,指向目的套接口的地址。
  •   addrlen:所指地址的長度。

三個函數用來接收數據:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
recvfrom()函數用於從已鏈接的套接口上接收數據,並捕獲數據發送源的地址。
參數:
  • sockfd:用來標識一個已鏈接套接口的描述字;
  • buf:接收數據緩衝區;
  • len:緩衝區長度;
  • flags:調用操做方式,通常狀況下爲0;
  • src_addr:指向裝有源地址緩衝區的指針;

關閉套接字描述符

 close函數用來關閉文件描述符:

#include <unistd.h>
int close(int fd);

  注意:close操做只是使相應socket描述字的引用計數-1,只有當引用計數爲0的時候,纔會觸發TCP客戶端向服務器發送終止鏈接請求。

 地址「重用」

缺省條件下,一個套接字不能與一個已在使用中的本地地址捆綁。但有時會須要「重用」地址。由於每個鏈接都由本地地址和遠端地址的組合惟一肯定,因此只要遠端地址不一樣,兩個套接口與一個地址捆綁並沒有大礙。爲了通知套接口實現不要由於一個地址已被一個套接口使用就不讓它與另外一個套接口捆綁,應用程序可在bind()調用前先設置SO_REUSEADDR選項。請注意僅在bind()調用時該選項才被解釋;故此無需(但也無害)將一個不會共用地址的套接字設置該選項,或者在bind()對這個或其餘套接口無影響狀況下設置或清除這一選項。

解決這個問題的方法是使用setsockopt()設置socket描述符的 選項SO_REUSEADDR爲1,表示容許建立端口號相同但IP地址不一樣的多個socket描述符。 在server代碼的socket()和bind()調用之間插入以下代碼:

int opt=1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

基於TCP的socket通訊基本流程:

  1. TCP服務器端依次調用socket()、bind()、listen()以後,就會監聽指定的socket地址了。
  2. TCP客戶端依次調用socket()、connect()以後就向TCP服務器發送了一個鏈接請求。
  3. TCP服務器監聽到這個請求以後,就會調用accept()函數取接收請求,這樣鏈接就創建好了。
  4. 以後就能夠開始網絡I/O操做了,即類同於普通文件的讀寫I/O操做。

創建一個基於TCP的socket API  :

服務器:

/*************************************************************************
 > File Name: server.c
 > Author:Lynn-Zhang 
 > Mail: iynu17@yeah.net
 > Created Time: Fri 29 Jul 2016 12:15:28 PM CST
 ************************************************************************/

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

static void usage(const char* proc)
{
    printf("Usage: %s [ip] [port]\n",proc);
}

void *thread_run(void *arg)
{
    printf("create a new thread\n");
    int fd=(int)arg;
    char buf[1024];
    while(1)
    {
        //服務器端將套接字描述符中到數據讀到buf並打印,再將本身的回覆寫入套接字描述符
        memset(buf,'\0',sizeof(buf));
        ssize_t _s=read(fd,buf,sizeof(buf)-1);
        if(_s>0)
        {
            buf[_s]='\0';
            printf("client:# %s",buf);
            printf("server:$ ");
            fflush(stdout);
            
            //服務器將回複寫入fd
            memset(buf,'\0',sizeof(buf));
            ssize_t _in=read(0,buf,sizeof(buf)-1);
            if(_in>=0)
            {
              buf[_in-1]='\0';
              write(fd,buf,strlen(buf));
            }
            printf("please wait ...\n");
        }
        else if(_s==0)
        {
            printf("client close...\n");
            break;
        }
        else
        {
            printf("read error ...\n");
            break;
        }
    }
    return (void*)0;
}

int main(int argc,char *argv[])
{
    //參數必須能構成完整的socket
    if(argc!=3)
    {
        usage(argv[0]);
        exit(1);
    }
    //創建服務器端socket
    int listen_sock=socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock<0)
    {
        perror("socket");
        return 1;
    }

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[2]));
    local.sin_addr.s_addr=inet_addr(argv[1]);
    
    int opt=1;
    if(setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))<0)
    {
        perror("setsockopet error\n");
        return -1;
    }

    //將套接字綁定到服務器端的ip地址和端口號綁定
    if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        return 2;
    }
    //創建監聽隊列,等待套接字的鏈接請求
    listen(listen_sock,5);

    struct sockaddr_in peer;
    socklen_t len=sizeof(peer);
    while(1)
    {
        //得到鏈接請求並創建鏈接
        int client_sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
        if(client_sock<0)
        {
            perror("accept faild ...\n");
            return 3;
        }
        printf("get a new link,socket -> %s:%d\n",inet_ntoa(peer.sin_addr));

        pthread_t id;
        pthread_create(&id,NULL,thread_run,(void*)client_sock);

        pthread_detach(id);

//        pid_t id=fork();
//        if(id==0)
//        {//child
//            char buf[1024];
//            while(1)
//            {
//                 //將監聽到的套接子描述符指定文件描述中的數據讀到buf中
//                memset(buf,'\0',sizeof(buf));
//                ssize_t _s=read(client_sock,buf,sizeof(buf)-1);
//                if(_s>0)
//                {
//                    buf[_s-1]='\0'
//                    printf("client:# %s\n",buf); 
//                    printf("server:$ ");
//                    fflush(stdout);
//                    memset(buf,'\0',sizeof(buf));
//                    ssize_t _s=read(0,buf,sizeof(buf)-1);
//                    if(_s>0)
//                    {                    
//                         buf[_s-1]='\0';
//                         write(client_sock,buf,strlen(buf));
//                    }
//                    else
//                    {
//                        printf("Fail !\n");
//                    }
//                }
//                else
//                {
//                      printf("read done...\n");
//                    break;
//                }
//            }
//
//        }
//        else
//        {//father
//            waitpid(-1,NULL,WNOHANG);
//        }
//
    }
    close(listen_sock);
    return 0;
}

  客戶端:

/*************************************************************************
 > File Name: client.c
 > Author:Lynn-Zhang 
 > Mail: iynu17@yeah.net
 > Created Time: Fri 29 Jul 2016 09:00:01 AM CST
 ************************************************************************/

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

static usage(const char* proc)
{
    printf("Usage: %s [ip] [port]\n",proc);
}

int main(int argc,char* argv[])
{
    //傳入的參數是一個完整的socket(ip地址+端口號)
    if(argc!=3)
    {
        usage(argv[0]);
        exit(1);
    }
    //創建一個套接字描述符
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }
    //IPv4因特網域(AF_INET)中,套接字地址用sockaddr_in表示
    struct sockaddr_in remote;
    remote.sin_family=AF_INET;   //socket通訊域
    remote.sin_port=htons(atoi(argv[2]));   //端口號
    remote.sin_addr.s_addr=inet_addr(argv[1]);  //ip地址
    //請求鏈接
    int ret=connect(sock,(struct sockaddr*)&remote,sizeof(remote));
    if(ret<0)
    {
        printf("connect failed ... ,errno is :%d,errstring is: %s\n",errno,strerror(errno));
        return 3;
    }
    printf("connect success ...\n");
    char buf[1024];
    while(1)
    {
        //從標準輸入將數據讀入buf中,再寫入sock中
        memset(buf,'\0',sizeof(buf));
        printf("client:# ");
        fflush(stdout);
        ssize_t _s=read(0,buf,sizeof(buf)-1);
        fflush(stdin);
        if(_s<0)
        {
            perror("read\n");
            break;
        }
        buf[_s]='\0';
        write(sock,buf,strlen(buf));
        if(strcmp(buf,"quit")==0)
        {
            printf("quit!\n");
            break;
        }

        _s=read(sock,buf,sizeof(buf));
        if(_s>0)
        {
            buf[_s]='\0';
            printf("server:$ %s\n",buf);
        }
    }
    close(sock);
    printf("sock close");
    return 0;
}

  服務器: 

客戶端:



 基於UDP協議的Socket編程

注意:

UDP沒有創建鏈接的過程!

建立一個基於udp協議的套接字,使用socket函數時第二個參數不能傳遞SOCK_STREAM,而是傳遞SOCK_DGRAM 

如建立一個基於IPv4地址族的UDP套接字: socket(AF_INET,SOCK_DGRAM, 0);

一般用於基於UDP協議的I/O通常使用 recvfrom 和 sendto 兩個函數進行數據收發!

 服務器:

/*************************************************************************
 > File Name: server.c
 > Author:Lynn-Zhang 
 > Mail: iynu17@yeah.net
 > Created Time: Wed 03 Aug 2016 01:14:30 PM CST
 ************************************************************************/

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

void usage(const char* proc)
{
    printf("Usage: %s [ip] [port]\n",proc);
}
int main(int argc,char* argv[])
{
    //要求輸出配套到套接字
    if(argc!=3)
    {
        usage(argv[0]);
        return 1;
    }
    //創建套接字描述符
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }
    
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(argv[2]));
    local.sin_addr.s_addr=inet_addr(argv[1]);
    //將套接字與地址綁定 
    int ret=bind(sock,(struct sockaddr*)&local,sizeof(local));
    if(ret<0)
    {
        perror("bind");
        return 3;
    }
    int done=0;
    struct sockaddr_in peer;
    socklen_t len=sizeof(peer);
    char buf[1024];

    while(!done)
    {
        memset(buf,'\0',sizeof(buf));
        recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len);
        printf("##########################\n");
        printf("get a client , socket:%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
        printf("client:%s,echo client!\n",buf);
        printf("##########################\n");
printf("server:");
        fflush(stdout);
        memset(buf,'\0',sizeof(buf));
        ssize_t _s= read(0,buf,sizeof(buf)-1);
        buf[_s]='\0';
        sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,len);
    }
    return 0;
}

  客戶端:

/*************************************************************************
 > File Name: client.c
 > Author:Lynn-Zhang 
 > Mail: iynu17@yeah.net
 > Created Time: Wed 03 Aug 2016 03:48:21 PM CST
 ************************************************************************/

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

static void usage(const char* proc)
{
    printf("Usage:%s [remote_ip] [remote_port]\n",proc);
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        usage(argv[0]);
        return 1;
    }

    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if(sock<0)
    {
        perror("socket");
        return 2;
    }

    struct sockaddr_in remote;
    remote.sin_family=AF_INET;
    remote.sin_port=htons(atoi(argv[2]));
    remote.sin_addr.s_addr=inet_addr(argv[1]);

    int done=0;
    char buf[1024];
    struct sockaddr_in peer;
    socklen_t len=sizeof(peer);
    while(!done)
    {
        printf("Please Enter: ");
        fflush(stdout);
        ssize_t _s=read(0,buf,sizeof(buf)-1);
        if(_s>0)
        {
            buf[_s-1]='\0';
            sendto(sock,buf,sizeof(buf),0,(struct sockaddr*)&remote,sizeof(remote));
            
            memset(buf,'\0',sizeof(buf));
            recvfrom(sock,buf,sizeof(buf),0,(struct sockaddr*)&peer,&len);
            printf("server echo %s\nsocket: %s:%d\n",buf,inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
        }
    }
    return 0;
}

 運行結果:

服務器:

客戶端:

 

 

部分參考:

吳秦   http://www.cnblogs.com/skynet/

《Unix 環境高級編程》

相關文章
相關標籤/搜索