TCP套接字編程


一.套接字(socket)函數

    圖1給出了在一個TCP客戶與服務器通信的流程。服務器首先啟動,稍后某個客戶啟動,它試圖連接到服務器。假設客戶給服務器發送一個請求,服務器處理該請求,並且給客戶發回一個相應。這個過程一直持續下去,知道客戶關閉連接的客戶端,從而給服務器發送一個EOF(文件結束)通知為止。服務器接着也關閉連接的服務器端,然手結束運行或者等待新的客戶連接。

圖1 客戶/服務器程序的套接字函數

 

1.socket函數

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

    為了執行一個I/O,一個進程必須做的第一件事情就是調用socket函數,指定期望的通信協議類型。其中family參數指明協議族,見表1;type參數指明套接字類型,見表2;protocol參數指明協議,見表3。 

family 說明
AF_INET IPv4協議
AF_INET6 IPv6協議
AF_LOCAL Unix域協議
AF_ROUTE 路由套接字
AF_KEY 密鑰套接字

表1 socket函數family常值

type 說明
SOCK_STREAM 字節流套接字
SOCK_DGRAM 數據報套接字
SOCK_SEQPACKET 有序分組套接字
SOCK_RAW 原始套接字

表2 socket函數type常值

protocol 說明
IPPROTO_TCP TCP傳輸協議
IPPROTO_UDP UDP傳輸協議
IPPROTO_SCTP SCTP傳輸協議

表3 socket函數AF_INET或AF_INET6的protocol常值

    socket函數在成功時返回一個套接字描述符(sockfd),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。

 

2.connect函數

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

    TCP客戶用connect函數來與服務器建立連接。第一個參數是socket函數返回的套接字描述符,第二個、第三個參數分別是一個指向套接字地址結構的指針和該結構的大小。下面給出IPv4和IPv6套接字的地址結構,幫助理解:

//IPv4套接字地址結構
struct in_addr {
    int_addr_t       s_addr;     /* 32-bit IPv4 address */
                                          /* network byte order */
};

struct sockaddr_in {
    uint8_t            sin_len;        /* len of structure */
    sa_family_t      sin_family;   /* AF_INET */
    in_port_t         sin_port;      /* 16-bit TCP or UDP port number */
                                             /* network byte order */
    struct in_addr  sin_addr;     /* 32-bit IPv4 address */
                                             /* network byte order */
    char                sin_zero[8]; /* unused */
};

//IPv6套接字地址結構 struct in6_addr { uint8_t s6_addr; /* 128-bit IPv6 address */ /* network byte order */ }; #define SIN6_LEN /* required for compile-time tests */ struct sockaddr_in6 { uint8_t sin6_len; /* len of structure */ sa_family_t sin6_family; /* AF_INET */ in_port_t sin6_port; /* 16-bit TCP or UDP port number */ /* network byte order */ uint32_t sin_flowinfo; /* flow information, undefined */ struct in6_addr sin6_addr; /* 32-bit IPv4 address */ /* network byte order */ uint32_t sin6_zero[8]; /* unused */ };

    客戶在調用函數connect前不必非得調用bind函數,因為如果需要的話,內核會確定源IP地址,並選擇一個臨時端口作為源端口。

    調用connect函數將激發TCP的三次握手過程,而且僅在連接建立成功或者出錯時才返回。三次握手建立連接會在后面介紹。

 

3.bind函數

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

    當我們調用socket函數時,返回的socket描述字它存在於協議族中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind函數,否則系統會自動隨機分配一個端口。

 

4.listen函數

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

    其中參數backlog規定了內核應該為相應套接字排隊的最大連接數。

    listen函數僅由TCP服務器調用,當socket函數創建一個套接字時,它被假設為一個主動套接字。而listen函數的作用就是把一個未連接套接字轉換成一個被動套接字,指示內核應接受指向該套接字的連接請求,即將該套接字從CLOSED狀態轉換到LISTEN狀態。

 

5.accept函數

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

     accept函數由TCP服務器調用,用於從已完成連接隊列對頭返回下一個已完成連接,如果已完成連接隊列為空,那么進程被投入睡眠。

    如果accept成功,那么其返回值是由內核自動生成的一個全新描述符,代表與所返回客戶的TCP連接。在accept函數中,第一個參數是監聽套接字描述符(由socket創建,然后用作bind和listen的第一個參數的描述符),它的返回值為已連接套接字描述符。一個服務器通常僅僅創建一個監聽套接字,它在該服務器的生命期內一直存在。內核為每個由服務器進程接受的客戶連接創建一個已連接套接字。當服務器完成對某個給定客戶的服務時,相應的已連接套接字就被關閉。

 

6.close函數

#include<sys/socket.h>
int close(int sockfd);

   close一個TCP套接字的默認行為是把該套接字標記成已關閉,然后立即返回到調用進程。該套接字描述符不能在有調用進程時使用。

 

二.傳輸控制協議(TCP)

1.TCP協議

       TCP是面向連接的通信協議,通過三次握手建立連接,所以只能用於端到端的通信。TCP提供一種可靠的數據流服務,當TCP向另一端發送數據時,它要求對端返回一個確認。如果沒有收到確認,TCP就自動重傳並等待,數次失敗后TCP才放棄。注意TCP並不保證數據一定會被對方端點接收,准確來說,它提供的是數據的可靠傳遞或故障的可靠通知。

2.TCP三次握手建立連接

  • 建立一個TCP連接會發生下述情形:
  • 服務器必須准備好接收外來的連接,即被動打開通常通過調用socket、bind和listen這3個函數來完成。
  • 客戶通過調用connect發起主動打開。這導致客戶TCP發送一個SYN(同步)分節,它告訴服務器客戶將在(待建立的)連接中發送的數據的初始序列號。
  • 服務器必須確認(ACK)客戶的SYN,同時自己也得發送一個SYN分節,它含有服務器將在同一連接中發送的數據的初始序列號。服務器在單個分節中發送SYN和對客戶SYN的ACK(確認)。
  • 客戶必須確認服務器的SYN。
  • 這種交換至少需要3個分組,因此稱為TCP的三次握手,如圖2所示。

圖2 TCP三次握手建立連接

    圖2中,客戶的初始序列號為J,服務器的初始序列號為K。ACK中的確認號是發送這個ACK的一端所期待的下一個序列號。因為SYN占據一個字節的序列號空間,所以每一個SYN的ACK中的確認號就是該SYN的初始序列號加1。類似的,每一個FIN(表示結束)的ACK中的確認號為該FIN的序列號加1。

3.TCP四次握手釋放連接

  • TCP建立一個連接需要3個分節,終止一個連接需4個分節。
  • 某個應用進程首先調用close,即該端執行主動關閉,該端的TCP發送一個FIN分節,表示數據發送完畢。
  • 接收到這個FIN的對端執行被動關閉。這個FIN由TCP確認,它的接收也作為一個文件結束符(EOF)傳遞給接收端應用進程(放在已排隊等候該應用進程接收的任何其他數據之后),因為FIN的接收意味着接收端應用程序在相應連接上再無額外數據接收。
  • 一段時間后,接收到這個文件結束符的應用進程將調用close關閉它的套接字,這導致它的TCP也發送一個FIN。
  • 接收這個最終的FIN的原發送端TCP(即執行主動關閉的那一端)確認這個FIN。

圖3 TCP四次握手釋放連接

    類似SYN,一個FIN也占據一個字節的序列號空間,因此每個FIN的ACK確認號就是這個FIN的序列號加1。

4.TCP狀態轉換圖

    TCP連接建立和種植的操作可用狀態狀態轉換圖表示,如圖4所示。

 圖4 TCP狀態轉換圖

    圖中給出了11種TCP狀態的名稱,這些狀態可使用netstat監視。

 

三.一個簡單的客戶/服務器程序

    寫了一個簡單的TCP客戶/服務器聊天程序作為全文總結:http://www.cnblogs.com/Rosanna/p/3494280.html

服務器代碼:

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
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include <unistd.h>
 
#define MAXSIZE 1024
#define PORT 8080
#define BACKLOG 10
 
int   main( int   argc, char   **argv)
{
     int   listenfd,connfd;
     struct   sockaddr_in servaddr,cliaddr;
     socklen_t len;
     char   message[MAXSIZE];
 
     if ((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     {
         perror ( "socket" );
         exit (1);
     }
     else   printf ( "socket create success!\n" );
 
     bzero(&servaddr, sizeof (servaddr));
     servaddr.sin_family=AF_INET;
     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
     servaddr.sin_port=htons(PORT);
 
     if ((bind(listenfd,( struct   sockaddr*)&servaddr, sizeof ( struct   sockaddr)))==-1)
     {
         perror ( "bind" );
         exit (1);
     }
     else   printf ( "bind success!\n" );
 
     if (listen(listenfd,BACKLOG)==-1)
     {
         perror ( "listen" );
         exit (1);
     }
     else   printf ( "sever is listening!\n" );
 
     for ( ; ; )
     {
         printf ( "*********開始聊天*********\n" );
         len= sizeof ( struct   sockaddr);
         if ((connfd=accept(listenfd,( struct   sockaddr*)&cliaddr,&len))==-1)
         {
             perror ( "accept" );
             exit (1);
         }
         else   printf ( "客戶端:%s: %d\n" ,inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
         for ( ; ; )
         {
             bzero(message,MAXSIZE);
             printf ( "輸入:" );
             fgets (message,MAXSIZE,stdin);
             if (!strncasecmp(message, "quit" , 4))
             {
                 printf ( "終止聊天!\n" );
                 break ;
             }
             else   len=send(connfd,message, strlen (message),0);
             if (len<0)
             {
                 printf ( "發送失敗" );
                 break ;
             }
 
             bzero(message,MAXSIZE);
             len=recv(connfd,message,MAXSIZE,0);
             if (len>0) printf ( "客戶端:%s" ,message);
             else   if (len<0) printf ( "接受消息失敗!\n" );
             else   printf ( "客戶端不在線!\n" );
         }
         close(connfd);
         printf ( "是否退出服務器[Y/N]:" );
         bzero(message,MAXSIZE);
         fgets (message,MAXSIZE,stdin);
         if (!strncasecmp(message, "Y" , 1))
         {
             printf ( "服務器已退出!" );
             break ;
         }
     }
     close(listenfd);
     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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
 
#define MAXSIZE 1024
#define PORT 8080
 
int   main( int   argc, char   **argv)
{
     int   sockfd;
     struct   sockaddr_in servaddr;
     socklen_t len;
 
     char   message[MAXSIZE];   
     
     if ((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
     {
         perror ( "socket" );
         exit (1);
     }
     else   printf ( "socket create success!\n" );
 
     bzero(&servaddr, sizeof (servaddr));
     servaddr.sin_family = AF_INET;
     servaddr.sin_port = htons(PORT);
 
     inet_aton(argv[1],&servaddr.sin_addr);
 
     if (connect(sockfd,( struct   sockaddr*)&servaddr, sizeof ( struct   sockaddr))==-1)
     {
         perror ( "connect" );
         exit (1);
     }
     else   printf ( "conncet success!\n" );
     
     for ( ; ; )
     {
         bzero(message,MAXSIZE);
         len=recv(sockfd,message,MAXSIZE,0);
         if (len>0)
             printf ( "服務器:%s" ,message);
         else
         {
             if (len<0) printf ( "接受消息失敗!\n" );
             else   printf ( "服務器已退出!\n" );
             break ;   
         }
 
         bzero(message,MAXSIZE);
         printf ( "輸入:" );
         fgets (message,MAXSIZE,stdin);
 
         if (!strncasecmp(message, "quit" , 4))
         {
             printf ( "client 請求終止聊天!\n" );
             break ;
         }  
         else
             len = send(sockfd,message, strlen (message),0);
         if (len<0)           
         {
             printf ( "消息發送失敗!\n" );
             break ;           
         }
     }
     close(sockfd);
     return   0;
}

 編譯:

1
2
gcc -Wall server.c -o server
gcc -Wall client.c -o client

 服務器運行結果:

1
2
3
4
5
6
7
8
9
10
11
12
./server
socket create success!
bind success!
sever is listening!
*********開始聊天*********
客戶端:127.0.0.1: 60355
輸入:hello
客戶端不在線!
輸入:quit
終止聊天!
是否退出服務器[Y/N]:Y
服務器已退出!

客戶端運行結果:

1
2
3
4
5
6
./client 127.0.0.1
socket create success!
conncet success!
服務器:hello
輸入:quit
client 請求終止聊天!

 


免責聲明!

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



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