套接字


網絡IPC:套接字

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

不同計算機(通過網絡相連)上運行的進程相互通信機制稱為網絡進程間通信(network IPC)。

在本地可以通過進程PID來唯一標識一個進程,但是在網絡中這是行不通的。其實TCP/IP協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機,而傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)構成套接字,就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。

套接字是通信端口的抽象!通過套接字網絡IPC接口,進程能夠使用該接口和其他進程通信。

幾個定義:

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

套接字描述符

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

要創建一個套接字,可以調用socket函數。

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

  參數:

作用:socket()用於創建一個socket描述符(socket descriptor),它唯一標識一個socket。


網絡字節序

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

1
2
3
4
5
#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函數來搞定這個問題:

1
2
3
#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表示。

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

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

1
2
3
4
5
struct  sockaddr
{
    sa_family_t sa_family;
    char         sa_data[14];
}

IPV4因特網域:

1
2
3
4
5
6
7
8
9
10
11
12
//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因特網域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//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域對應的是: 

1
2
3
4
5
6
7
#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地址。

1
2
3
4
5
6
7
8
9
#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* 類型!

1
2
3
4
5
#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 來宣告可以接收連接請求!

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

參數:sockfd為要監聽的socket描述字,backlog為相應socket可以排隊的最大連接個數  

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

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


連接

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

1
2
3
#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函數獲得連接請求並建立連接

1
2
3
#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來通過套接字通信了。

1
2
3
#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個傳遞數據的套接字函數中的一個。

三個函數用來發送數據:

1
2
3
4
5
#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);

  三個函數用來接收數據:

1
2
3
4
5
#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);

關閉套接字描述符

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

1
2
#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()調用之間插入如下代碼:

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

socket通信基本流程:

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


 

代碼示例

建立一個基於TCP的socket API

服務器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/*************************************************************************
  > 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;
}

 客戶端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*************************************************************************
  > 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;
}

服務器: 

客戶端:

 

本篇總結若有不足,希望指正 (。⌒∇⌒)

 

部分參考:

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

《Unix 環境高級編程》

 

 

 

 

分類:  Linux


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM