TCP套接字編程模型及實例
睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接
摘要:
本文講述了TCP套接字編程模塊,包括服務器端的創建套接字、綁定、監聽、接受、讀/寫、終止連接,客戶端的創建套接字、連接、讀/寫、終止連接。先給出實例,進而結合代碼分析。
PS:本文權當復習套接字編程的讀書筆記。
一、TCP套接字編程模型
同一臺計算機上運行的進程可以利用管道、消息隊列、信號量、共享內存等進行相互通信,不同計算機上運行的進程可以通過套接字網絡IPC接口進行相互通信。套接字編程基本步驟如下圖所示:
圖 TCP套接字編程模型[1]
二、源代碼
本實例旨在實現簡單的echo服務,客戶端發送數據給服務端,在服務端打印出來并且回發給客戶端,并在客戶端顯示。
TCP_socket_programming_example源文件 TCP_socket_programming_example.rar
2.1 TCP服務端
//filename:TCPserver.c
#include
#include
#include
#include
#define BACKLOG 10
#define BUFFER_SIZE 1024
int main(int argc, char *argv[])
{
if(2 != argc)
{
printf("Usage:%s portnumber\n", argv[0]);
return - 1;
}
/***1.create a socket***/
int fd_server = socket(AF_INET, SOCK_STREAM, 0); //TCP
if( - 1 == fd_server)
{
printf("%s\n", strerror(errno));
return - 1;
}
/***2.bind the socket***/
int listen_port = atoi(argv[1]);
struct sockaddr_in addr_server;
//memset(&addr_server, 0, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(listen_port);
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd_server, (struct sockaddr*) &addr_server, sizeof(addr_server)) == - 1)
{
printf("%s\n", strerror(errno));
return - 1;
}
/***3.listen the socket***/
if(listen(fd_server, BACKLOG) == - 1)
{
printf("%s\n", strerror(errno));
return - 1;
}
/***4.accept the requirement of some client***/
struct sockaddr_in addr_client;
int len_addr_client = sizeof(addr_client);
int fd_client = accept(fd_server, (struct sockaddr*) &addr_client, &len_addr_client);
if( - 1 == fd_client)
{
printf("%s\n", strerror(errno));
return - 1;
}
/****5.serve the client******/
char buf[BUFFER_SIZE];
int size;
while(1)
{
/***read from client***/
size = recv(fd_client, buf, sizeof(buf), 0);
buf[size] = '\0';
printf("%s\n", buf);
/***write to client***/
size = send(fd_client, buf, strlen(buf), 0);
}
/****6.close the socket******/
close(fd_server);
close(fd_client);
}
2.2 TCP客戶端
點擊(此處)折疊或打開
//filename:TCPclient.c
#include
#include
#include
#include
#define BUFFER_SIZE 1024
int main(int argc, char *argv[])
{
if(3 != argc)
{
printf("Usage:%s hostname portnumber\n", argv[0]);
return - 1;
}
/***1.create a socket***/
int fd_client = socket(AF_INET, SOCK_STREAM, 0); //TCP
if( - 1 == fd_client)
{
printf("%s\n", strerror(errno));
return - 1;
}
/***2.connect to the server***/
int portnumber = atoi(argv[2]);
struct sockaddr_in addr_server;
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(portnumber);
if(0 == inet_pton(AF_INET, argv[1], (void*) &addr_server.sin_addr.s_addr))
{
printf("Invalid address.\n");
return - 1;
}
if(connect(fd_client, (struct sockaddr*) &addr_server, sizeof(addr_server)) == - 1)
{
printf("%s\n", strerror(errno));
return - 1;
}
/****3.get the server******/
char buf[BUFFER_SIZE];
int size;
while(1)
{
/***write to server***/
scanf("%s", buf);
size = send(fd_client, buf, strlen(buf), 0);
/***read from server***/
size = recv(fd_client, buf, BUFFER_SIZE, 0);
buf[size] = '\0';
printf("%s\n", buf);
}
/****4.close the socket******/
close(fd_client);
}
2.3 測試結果
$ ./TCPserver 2000
$ ./TCPclient 127.0.0.1 2000
三、源碼分析
3.1 創建套接字
int socket(int domain, int type, int protocol);//成功返回套接字描述符.出錯返回-1
這一步事實上是確定通信特征,各個域domain有自己的格式表示地址,以AF_開頭(address family);type確定套接字類型,如數據報、字節流;協議protocol對同一個域和套接字類型支持的多個協議進行選擇,通常為0,即按給定的域和套接字類型選擇默認的協議。典型的TCP、UDP如下:
TCP:(AF_INET, SOCK_DGRAM, 0)
UDP:(AF_INET, SOCK_STREAM, 0)
注:
盡管套接字本質是文件描述符,但不是所有用于文件操作的函數都能用于套接字操作,比如lseek,套接字不支持文件偏移量。
3.2 綁定
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);//成功返回0.出錯返回-1
bind函數用于將地址綁定到一個套接字。服務器需要給一個接收客戶端請求套接字綁定一個眾所周知的地址,而客戶端可以讓系統選一個默認地址綁定(無須綁定)。
(1) 套接字地址sockaddr_in
在IPv4因特網域AF_INET中,套接字地址用結構sockaddr_in表示,如下:
struct sockaddr_in
{
sa_family_t sin_family; //unsigned short 地址族
in_port_t sin_sport; //uint16_t
struct in_addr sin_addr; //IPv4
};
struct in_addr
{
in_addr_t s_addr; //uint32_t
};
注:
初始化sockaddr_in結構體時,因為sin_port和sin_addr被封裝在網絡傳輸,所以端口號和地址必須用網絡字節序;而sin_family只是被內核用來決定數據結構包含什么類型的地址,沒有發送到到網絡,應該是本機字節順序。處理器與網絡字節序之間轉換函數為htonl、htons、ntohl、ntohs(h指host主機,n指network網絡,l指long32位,s指short16位)。
理論上,端口號可以是0~65535,但1~1023已由IANA管理,綁定時端口號不少于1024[2]。
此處的地址s_addr是二進制地址格式,如果參數是點分十進制字符串表示,則需通過函數inet_ntop(將網絡字節序的二進制地址轉換成點分十進制字符串表示)、inet_pton進行相互轉換。其轉換過程如下:
127.0.0.1 --> 7F.0.0.1 --> 100007F=16777343(網絡字節序為大端)
如果地址s_addr為ANADDR_ANY,套接字端點可以被綁定到所有系統網絡接口,即可以收到這個系統所安裝的所有網卡的數據包。
(2) 通用地址格式sockaddr
地址格式與特定的通信域有關(如AF_INET、AF_INET6),為使不同地址格式地址能夠傳入套接字函數,地址被強制轉換成通用的地址結構sockaddr,如下(以Linux為例):
struct sockaddr
{
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
3.3 監聽listen
int listen(int sockfd, int backlog);//成功返回0,出錯返回-1
一旦服務器調用listen,套接字就能接收連接請求。backlog用于表示該進程所要入隊的連接請求數量,實際值由系統決定,但上限由SOMAXCONN指定。一旦隊列滿,系統會拒絕多余連接請求。
3.4 接受連接請求accept
//成功返回套接字描述符,出錯返回-1
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
使用accept獲得連接請求并建立連接,新的套接字描述符連接到調用connect的客戶端。傳給accept的原始套接字(sockfd)沒有關聯到這個連接,而是接收保持可用狀態并接受其他請求連接,這樣做是為了使新的套接字描述符和原始套接字具有相同的地址族domain和套接字類型type。
如果服務器調用accept并且當前沒有連接請求,服務器會阻塞直到一個請求到來。如果不關心客戶端標識,可以將參數addr和len設為NULL。
注:
關鍵字restrict是C99新引入的,所有修改該指針所指向內容的操作全部都是基于(base on)該指針的,即不存在其它進行修改操作的途徑;從而幫助編譯器進行更好的代碼優化,生成更有效率的匯編代碼[4]。
3.5 建立連接connect
//成功返回0,出錯返回-1
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
addr是想與之通信的服務器地址,如果sockfd沒有綁定到一個地址,connect會給調用者綁定一個默認的地址。成功連接需要以下條件:要連接的機器開啟且正在運行,服務器綁定到一個想與之連接的地址,服務器的等待連接隊列有足夠的空間。
3.6 讀取數據
ssize_t read(int fd, void *buf, size_t nbytes); //成功返回讀到的字節數,已到文件末尾返回0,出錯返回-1
ssize_t recv(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回字節計數的消息長度,無可用消息或對方已經按序結束返回0,出錯返回-1
ssize_t recvfrom(int sockfd,void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socketlen_t *restruct addrlen); //成功返回字節計數的消息長度,無可用消息或對方已經按序結束返回0,出錯返回-1
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);//成功返回字節計數的消息長度,無可用消息或對方已經按序結束返回0,出錯返回-1
可以使用read通過套接字通信,但read只能交換數據,若想指定選項、從多個客戶端接收數據包,則需選擇套接字函數recv(指定標志控制接收數據的方式)、recvfrom(得到數據發送者的源地址)、resvmsg(將接收到數據送入多個緩沖區或接收輔助數據)。
3.7 寫入數據
ssize_t write(int fd, void *buf, size_t count); //成功返回寫入字節數,出錯返回-1
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); //成功返回發送的字節數,出錯返回-1
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *destaddr, socklen_t destlen); //成功返回發送的字節數,出錯返回-1
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); //成功返回發送的字節數,出錯返回-1
注:send與sendto的flags含義相同,sendmsg的flags與前兩者不同
可以使用write通過套接字通信,但write只能交換數據,若想指定選項、發送帶外數據,則需選擇套接字函數send(指定標志改變處理傳輸數據的方式)、sendto(允許無連接的套接字上指定一個目標地址)、sendmsg(指定多重緩沖傳輸數據)。
3.8 終止連接
int close(int fd); //成功返回0,出錯返回-1
int shutdown(int sockfd, int how);//成功返回0,出錯返回-1
關閉套接字close只有在最后一個活動引用被關閉后才釋放網絡端點,而shutdown提供更精細的控制,套接字通信是雙向的,可以用shutdown禁止套接字上的輸入/輸出,即how為SHUT_RD、SHUT_WR、SHUT_RDWR。除此之外,shutdown允許使一個套接字處于不活動狀態(不管引用它的文件描述符數目多少),便于復制一個套接字(如dup)。
參考資料:
[1] 百度文庫《互聯網絡程序設計第3章》
[2] 《UNIX環境高級編程》[M].
[3] 維基百科詞條:TCP/UDP端口列表
[4] 博文《C99中的restrict關鍵字》
套接字調用標志源文件 套接字調用標志.xls
from:http://blog.chinaunix.net/uid-9112803-id-3199813.html
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成