一.套接字(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 請求終止聊天!
|