1. 程式人生 > >3、【網路程式設計】Socket程式設計

3、【網路程式設計】Socket程式設計

一、Socket定義

    Socket:在TCP/IP協議中,“IP地址+TCP或UDP埠號”唯 一標識網路通訊中的一個程序,所以“IP地址+埠號”就稱為socket。 在TCP協議中,建立連線的兩個程序各自有一個socket來標識,那麼這兩個socket組成的socket pair就唯一標識一個連線。 TCP/IP協議最早在BSD UNIX上實現,為TCP/IP協議設計的應用層程式設計介面稱為socket API。

二、TCP套接字程式設計模型

1、伺服器端流程簡:

    (1)建立套接字(socket);

    (2)將套接字繫結到一個本地地址和埠上(bind);

    (3)將套接字設定為監聽模式,準備接受客戶端請求(listen);

    (4)阻塞等待客戶端請求到來。當請求到來後,接受連線請求,返回一個新的對應於此客戶端連線的套接字sockClient(accept);

    (5)用返回的套接字sockClient和客戶端進行通訊(send/recv);

    (6)返回,等待另一個客戶端請求(accept);

    (7)關閉套接字(close);

2、客戶端流程:

    (1) 建立套接字(socket);

    (2) 向伺服器發出連線請求(connect);

    (3) 和伺服器進行通訊(send/recv);

    (4) 關閉套接字(close);

具體流程如下圖所示:

三、Socket基本操作

1、建立套接字,socket函式
    int socket(int domain, int type, int protocol)
    //成功時返回檔案控制代碼,失敗時返回-1.
    //domain 套接字中使用的協議族資訊
    //type 套接字資料傳輸型別資訊
    //protocol 計算機間通訊使用的協議資訊

(1)domain所選的協議族

名稱 協議族
PF_INET IPV4網際網路協議族
PF_INET6 IPV6網際網路協議族

(2)type套接字型別

type型別 作用
SOCK_STREAM 面向連線的套接字
SOCK_DGRAM 面向訊息的套接字

(3)protacol協議的最終選擇

    同一協議中存在多個數據型別傳輸方式相同的協議,就通過protocol區分最終協議,如果只有一個,預設為0。

2、分配IP和埠,bind函式
    int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen) 
    //成功返回0,失敗返回-1 
    //sockfd 套接字檔案描述符 
    //myaddr 結構體變數地址值,包括IP地址和埠號 
    //addrlen 結構體變數的長度

    bind()函式把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和埠號組合賦給socket。

    (1)sockfd:即socket描述字,它是通過socket()函式建立的,唯一標識一個socket。bind()函式就是將給這個描述字繫結一個名字。

    (2)addr:一個const struct sockaddr *指標,指向要繫結給sockfd的協議地址。這個地址結構根據地址建立socket時的地址協議族的不同而不同,如IPV4對應的是:

struct sockaddr_in {
    /*address family:AF_INET*/
    sa_family_t sin_family; 
    
    /*port in network byte order */
    in_port_t sin_port; 
    
    /* internet address */
    struct in_addr sin_addr;   
};
/* Internet address. */
struct in_addr {
    /* address in network byte order */
    uint32_t s_addr;     
};

    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)addrlen:對應的是地址的長度。

    通常伺服器在啟動的時候都會繫結一個眾所周知的地址(如ip地址+埠號),用於提供服務,客戶就可以通過它來接連伺服器;而客戶端就不用指定,有系統自動分配一個埠號和自身的ip地址組合。這就是為什麼通常伺服器端在listen之前會呼叫bind(),而客戶端就不會呼叫,而是在connect()時由系統隨機生成一個。

3、listen()/connect()函式

    如果作為一個伺服器,在呼叫socket()、bind()之後就會呼叫listen()來監聽這個socket,如果客戶端這時呼叫connect()發出連線請求,伺服器端就會接收到這個請求。

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

    listen函式的第一個引數即為要監聽的socket描述字,第二個引數為相應socket可以排隊的最大連線個數。socket()函式建立的socket預設是一個主動型別的,listen函式將socket變為被動型別的,等待客戶的連線請求

    connect函式的第一個引數即為客戶端的socket描述字,第二引數為伺服器的socket地址,第三個引數為socket地址的長度。客戶端通過呼叫connect函式來建立與TCP伺服器的連線

4、accept()函式

     TCP伺服器端依次呼叫socket()、bind()、listen()之後,就會監聽指定的socket地址了。TCP客戶端依次呼叫socket()、connect()之後就想TCP伺服器傳送了一個連線請求。TCP伺服器監聽到這個請求之後,就會呼叫accept()函式取接收請求,這樣連線就建立好了。之後就可以開始網路I/O操作了,即類同於普通檔案的讀寫I/O操作。

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

     accept函式的第一個引數為伺服器的socket描述字,第二個引數為指向struct sockaddr *的指標,用於返回客戶端的協議地址,第三個引數為協議地址的長度。如果accpet成功,那麼其返回值是由核心自動生成的一個全新的描述字,代表與返回客戶的TCP連線。

     注意:accept的第一個引數為伺服器的socket描述字,是伺服器開始呼叫socket()函式生成的,稱為監聽socket描述字;而accept函式返回的是已連線的socket描述字。一個伺服器通常通常僅僅只建立一個監聽socket描述字,它在該伺服器的生命週期內一直存在。核心為每個由伺服器程序接受的客戶連線建立了一個已連線socket描述字,當伺服器完成了對某個客戶的服務,相應的已連線socket描述字就被關閉。

5、read()/write()函式

    萬事具備只欠東風,至此伺服器與客戶已經建立好連線了。可以呼叫網路I/O進行讀寫操作了,即實現了網咯中不同程序之間的通訊!網路I/O操作有下面幾組:

    read()/write()
    recv()/send()
    readv()/writev()
    recvmsg()/sendmsg()
    recvfrom()/sendto()

    開發語言不同可能讀寫函式也就不同,只要把自己想要傳送的訊息,以位元組流的方式寫入Socket或者從Socket讀出來即可實現網路的I/O操作。

6、close()函式

    在伺服器與客戶端建立連線之後,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完開啟的檔案要呼叫fclose關閉開啟的檔案。

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

    close一個TCP socket的預設行為時把該socket標記為以關閉,然後立即返回到呼叫程序。該描述字不能再由呼叫程序使用,也就是說不能再作為read或write的第一個引數。

    注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發TCP客戶端向伺服器傳送終止連線請求。

三、Socket程式設計例項

    咋Linux上實現的一個簡單的socket通訊例項:
Server端:

#include<stdio.h>
//下面兩個標頭檔案是使用socket必須引入的
#include<sys/types.h>
#include<sys/socket.h>

#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
 //啟動伺服器通訊埠
int startup(int _port,const char* _ip)
{
    //socket()函式開啟一個網路通訊視窗,成功則返回一個檔案描述符,應用程式可以向讀寫檔案一樣用read/write在網路上轉發資料。
    //若調用出錯則返回-1
    //socket()函式的三個引數:協議型別, 套接字型別, 協議型別的常量或設定為0
    //AF_INET(IPv4協議)  SOCK_STREAM位元組流套接字
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(1);
    }
 
    struct sockaddr_in local;//網路程式設計中常用的資料結構
    local.sin_family=AF_INET;//IPVC4地址族
    local.sin_port=htons(_port);//將埠地址轉換為網路二進位制數字
    local.sin_addr.s_addr=inet_addr(_ip);//將網路地址轉換為網路二進位制數字
 
    socklen_t len=sizeof(local);
   //繫結套接字:成功返回0, 失敗返回-1
    //功能:將sock和local繫結在一起,使得sock這個用於網路通訊的問價描述符監聽local所描述的地址和埠
   if(bind(sock,(struct sockaddr*)&local,len)<0)
   {
     perror("bind");
     exit(2);
    }
    //listen(int sockfd, int backlog)監聽函式,sockfd為要監聽的socket套接字,backlog為可以排隊的最大連線數。
    //socket()函式建立的socket預設是一個主動型別的,listen函式將socket變為被動型別的,等待客戶的連線請求。
    //監聽成功返回0, 失敗返回-1
    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(3);
    }
   return sock;
}
int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("Usage: [local_ip] [local_port]",argv[0]);
        return 3;
    }
    //啟動伺服器套接字listen_socket
    int listen_socket=startup(atoi(argv[2]),argv[1]);
 
    struct sockaddr_in remote;
    socklen_t len=sizeof(struct sockaddr_in);
 
    while(1)
    {
    //accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen)函式,
    //sockfd為伺服器的socket套接字,addr為客戶端協議地址,addrlen為協議地址的長度,
    //如果accept成功,則返回一個由核心自動生成的全新套接字,代表與返回客戶的TCP連線
        int socket=accept(listen_socket,(struct sockaddr*)&remote,&len);
        if(socket<0)
        {
            perror("accept");
            continue;
        }
    //inet_ntoa:將網路二進位制數字轉換為網路地址      
    //ntohs:將網路二進位制數字轉換為埠號
        printf("client,ip:%s,port:%d\n",inet_ntoa(remote.sin_addr)\
               ,ntohs(remote.sin_port));
    
 
        char buf[1024];
        while(1)
        {
        //呼叫網路I/O進行讀寫
            ssize_t _s=read(socket,buf,sizeof(buf)-1);
            if(_s>0)
            {
               buf[_s]=0;
               printf("client# %s\n",buf);   
            }
            else
            {
               printf("client is quit!\n");
               break;
            }
            
        }
    //關閉套接字
        close(socket);
    }    
    return 0;
}

Client端:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
 
static 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 3;
    }
    
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(1);
    }
 
    struct sockaddr_in server;
    server.sin_family=AF_INET;
    server.sin_port=htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    //connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
    //sockfd為要監聽的socket套接字,addr引數為伺服器的socket地址,addrlen為socket地址的長度。
    //客戶端通過呼叫connect函式來建立與TCP伺服器的連線。
    if(connect(sock,(struct sockaddr*)&server,(socklen_t)sizeof(server))<0)
    {
        perror("connect");
        exit(2);
    }
 
    char buf[1024];
 
    while(1)
    {
        printf("send#");
        fflush(stdout);
        ssize_t _s=read(0,buf,sizeof(buf)-1);
        buf[_s-1]=0;
        write(sock,buf,_s);
    }
    
    close(sock);
    return 0;
 
}