套接字編程


套接字是一個雙向通信設備,可用於同一台主機上不同進程之間的通信,也可用於溝通
位於不同主機的進程。套接字是本章中介紹的所有進程間通信方法中唯一允許跨主機通信的
方式。Internet 程序,如Te l n e t 、rlogin 、FTP 、talk 和萬維網都是基於套接字的。
例如,你可以用一個Te l n e t 程序從一台網頁服務器獲取一個萬維網網頁,因為它們都使
用套接字作為網絡通信方式
。可以通過執行telnet www.codesourcery.com 80 連接到
位於www.codesourcery.com 主機的網頁服務器。魔數80 指明了連接的目標進程是運行於
www.codesourcery.com 的網頁服務器而不是其它什么進程。成功建立連接后,試着輸入
GET / 。這會通過套接字發送一條消息給網頁服務器,而相應的回答則是服務器將主頁的
HTML 代碼傳回然后關閉連接——例如:
% telnet www.codesourcery.com 80
Trying 206.168.99.1...
Connected to merlin.codesourcery.com (206.168.99.1).

Escape character is '^]'.
GET /
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=iso-8859-1">
...

5.5.1 套接字概念
當你創建一個套接字的時候你需要指定三個參數:通信類型,命名空間和協議。
通信類型決定了套接字如何對待被傳輸的數據同時指定了參與傳輸過程的進程數量。
當數據通過套接字發送的時候會被分割成段落,這些段落分別被稱為一個包(packet )。通
信類型決定了處理這些包的方式,以及為這些包定位目標地址的方式。


· 連接式(Connection style )通信保證所有包都以發出時的順序被送達。如果由於網
絡的關系出現包丟失或順序錯亂,接收端會自動要求發送端重新傳輸。


連接類型的套接字可想象成電話:發送和接收端的地址在開始時連接被建立的時候
都被確定下來。


· 數據報式(Datagram style)的通信不確保信息被送到,也不保證送到的順序。數
據可能由於網絡問題或其它情況在傳輸過程中丟失或重新排序。每個數據包都必須
標記它的目標地址,而且不會被保證送到。系統僅保證“盡力”做到,因此數據包
可能消失,或以與發出時不同的順序被送達。


數據報類型的通信更類似郵政信件。發送者為每個單獨信息標記收信人地址。

套接字的命名空間指明了套接字地址的書寫方式套接字地址用於標識一個套接字連接
的一個端點。例如,在“本地命名空間”中的套接字地址是一個普通文件。而在“Internet
命名空間”中套接字地址由網絡上的一台主機的Internet 地址(也被稱為 Internet 協議地址
或IP 地址)和端口號組成。端口號用於區分同一台主機上的不同套接字。


協議指明了數據傳輸的方式。常見的協議有如下幾種:TCP/IP,Internet 上使用的最主
要的通信協議;AppleTalk 網絡協議;UNIX 本地通信協議等。通信類型、命名空間和協議
三者的各種組合中,只有部分是有效的。

5.5.2 系統調用
套接字比之前介紹的任何一種進程間通信方法都更具彈性。這里列舉了與套接字相關的
系統調用:
socket——創建一個套接字
close ——銷毀一個套接字
connect——在兩個套接字之間創建連接
bind——將一個服務器套接字綁定一個地址
listen——設置一個套接字為接受連接狀態
accept——接受一個連接請求並為新建立的連接創建一個新的套接字

套接字通常被表示為文件描述符。

創建和銷毀套接字
Socket 和close 函數分別用於創建和銷毀套接字。當你創建一個套接字的時候,需指
明三種選項:命名空間,通信類型和協議。利用PF_ 開頭(標識協議族,protocol families )
的常量指明命名空間類型。例如,PF_LOCAL 或PF_UNIX 用於標識本地命名空間,而
PF_INET表示 Internet 命名空間。用以SOCK_開頭的常量指明通信類型。SOCK_STREAM
表示連接類型的套接字,而SOCK_DGRAM表示數據報類型的套接字。
第三個參數,協議,指明了發送和接收數據的底層機制。每個協議僅對一種命名空間和
通信類型的組合有效。因為通常來說,對於某種組合都有一個最合適的協議,為這個參數指
定0 通常是最合適的選擇。如果socket 調用成功則會返回一個表示這個套接字的文件描述
符。與操作普通文件描述符一樣,你可以通過read 和write 對這個套接字進行讀寫。當
你不再需要它的時候,應調用close 刪除這個套接字。

調用connect
要在兩個套接字之間建立一個連接,客戶端需指定要連接到的服務器套接字地址,然后
調用connect。客戶端指的是初始化連接的進程,而服務端指的是等待連接的進程。客戶
端調用connect以在本地套接字和第二個參數指明的服務端套接字之間初始化一個連接。
第三個參數是第二個參數中傳遞的標識地址的結構的長度,以字節計。套接字地址格式隨套
接字命名空間的不同而不同。

發送信息
所有用於讀寫文件描述符的技巧均可用於讀寫套接字。關於 Linux 底層 I/O 函數及一些
相關使用問題的討論請參考附錄B 。而專門用於操作套接字的 send 函數提供了write 之外
的另一種選擇,它提供了write 所不具有的一些特殊選項;參考send 的手冊頁以獲取更
多信息。

5.5.3 服務器
服務器的生命周期可以這樣描述:創建一個連接類型的套接字,綁定一個地址,調用
listen 將套接字置為監聽狀態,調用accept 接受連接,最后關閉套接字。數據不是直接
經由服務套接字被讀寫的;每次當程序接受一個連接的時候,Linux 會單獨創建一個套接字
用於在這個連接中傳輸數據。在本節中,我們將介紹bind、listen 和accept。
要想讓客戶端找到,必須用bind 將一個地址綁定到服務端套接字。Bind 函數的第一
個參數是套接字文件描述符。第二個參數是一個指針,它指向表示套接字地址的結構。它的
格式取決於地址族。第三個參數是地址結構的長度,以字節計。將一個地址綁定到一個連接
類型的套接字之后,必須通過調用 listen 將這個套接字標識為服務端。Listen 的第一個
參數是套接字文件描述符。第二個參數指明最多可以有多少個套接字處於排隊狀態。當等待
連接的套接字超過這個限度的時候,新的連接將被拒絕。它不是限制一個服務器可以接受的
連接總數;它限制的是被接受之前允許嘗試連接服務器的客戶端總數
服務端通過調用accept 接受一個客戶端連接。第一個參數是套接字文件描述符。第二
個參數是一個指向套接字地址結構的指針;接受連接后,客戶端地址將被寫入這個指針指向
的結構中。第三個參數是套接字地址結構體的長度,以字節計。服務端可以通過客戶端地址
確定是否希望與客戶端通信。調用 accept 會創建一個用於與客戶端通信的新套接字,並返
回對應的文件描述符。原先的服務端套接字繼續保持監聽連接的狀態。用 recv 函數可以從
套接字中讀取信息而不將這些信息從輸入序列中刪除。它在接受與 read 相同的一組參數的

基礎上增添了一個FLAGS參數。指定 FLAGS為MSG_PEEK可以使被讀取的數據仍保留
在輸入序列中。

5.5.4 本地套接字
要通過套接字連接同一台主機上的進程,可以使用符號常量PF_LOCAL 和PF_UNIX
所代表的本地命名空間。它們被稱為本地套接字(local sockets )或者UNIX 域套接字
(UNIX-domain sockets )。它們的套接字地址用文件名表示,且只在建立連接的時候使用。
套接字的名字在struct sockaddr_un 結構中指定。你必須將 sun_family 字段設置
為AF_LOCAL以表明它使用本地命名空間。該結構中的sun_path 字段指定了套接字使用
的路徑,該路徑長度必須不超過108 字節。而struct sockaddr_un 的實際長度應由
SUN_LENG 宏計算得到。可以使用任何文件名作為套接字路徑,但是進程必須對所指定的
目錄具有寫權限,以便向目錄中添加文件。如果一個進程要連接到一個本地套接字,則必須
具有該套接字的讀權限。盡管多台主機可能共享一個文件系統,只有同一台主機上運行的程

序之間可以通過本地套接字通信。
本地命名空間的唯一可選協議是0 。
因為它存在於文件系統中,本地套接字可以作為一個文件被列舉。如下面的例子,注意
開頭的s:
% ls -l /tmp/socket
srwxrwx--x 1 user group 0 Nov 13 19:18 /tmp/socket
當結束使用的時候,調用unlink 刪除本地套接字。

5.5.5 使用本地套接字的示例程序
我們用兩個程序展示套接字的使用。列表5.10 中的服務器程序建立一個本地命名空間
套接字並通過它監聽連接。當它連接之后,服務器程序不斷從中讀取文本信息並輸出這些信
息直到連接關閉。如果其中一條信息是“quit”,服務器程序將刪除套接字,然后退出。服
務器程序socket-server 接受一個標識套接字路徑的命令行參數。

代碼列表 5.10 (socket-server.c)本地命名空間套接字服務器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

/* 不斷從套接字讀取並輸出文本信息直到套接字關閉。當客戶端發送“quit”消息的
時候返回非 0 值,否則返回 0。*/
int server (int client_socket)
{
while (1) {

int length;

char* text;

/* 首先,從套接字中獲取消息的長度。如果 read 返回 0 則說明客戶端關閉了連
接。*/
if (read (client_socket, &length, sizeof (length)) == 0)
return 0;
/* 分配用於保存信息的緩沖區。*/
text = (char*) malloc (length);
/* 讀取並輸出信息。*/
read (client_socket, text, length);
printf (“%s\n”, text);
/* 如果客戶消息是“quit”,我們的任務就此結束。*/
if (!strcmp (text, “quit”)) {
/* 釋放緩沖區。*/
free (text);
return 1;
}

/* 釋放緩沖區。*/
free (text);
/* 譯者注:合並了勘誤中的提示,並增加此返回語句。*/
return 0;
}
}

int main (int argc, char* const argv[])
{
const char* const socket_name = argv[1];
int socket_fd;
struct sockaddr_un name;  //套接字名稱
int client_sent_quit_message;

/* 創建套接字。*/
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0); //PF_LOCAL 本地命名空間   連接型套接字
/* 指明這是服務器。*/
name.sun_family = AF_LOCAL;//你必須將 sun_family 字段設置為AF_LOCAL以表明它使用本地命名空間。

strcpy (name.sun_path, socket_name); 

bind (socket_fd, &name, SUN_LEN (&name));
/* 監聽連接。*/
listen (socket_fd, 5); //

/*通過調用 listen 將這個套接字標識為服務端。Listen 的第一個
參數是套接字文件描述符。第二個參數指明最多可以有多少個套接字處於排隊狀態。當等待
連接的套接字超過這個限度的時候,新的連接將被拒絕。它不是限制一個服務器可以接受的
連接總數;它限制的是被接受之前允許嘗試連接服務器的客戶端總數。*/



/* 不斷接受連接,每次都調用 server() 處理客戶連接。直到客戶發送“quit”消
息的時候退出。*/
do {

struct sockaddr_un client_name;
socklen_t client_name_len;
int client_socket_fd;

/* 接受連接。*/
client_socket_fd = accept (socket_fd, &client_name, &client_name_len); 
/* 處理連接。*/
client_sent_quit_message = server (client_socket_fd);
/* 關閉服務器端連接到客戶端的套接字。*/
close (client_socket_fd);
}
while (!client_sent_quit_message);

/* 刪除套接字文件。*/
close (socket_fd);
unlink (socket_name);

return 0;
}


列表 5.11 中的客戶端程序將連接到一個本地套接字並發送一條文本消息。本地套接字
的路徑和要發送的消息由命令行參數指定。

代碼列表 5.11 (socket-client.c )本地命名空間套接字客戶端

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

/* 將 TEXT 的內容通過 SOCKET_FD 代表的套接字發送。*/
void write_text (int socket_fd, const char* text)
{
/* 輸出字符串(包含結尾的 NUL 字符)的長度。*/
int length = strlen (text) + 1;
write (socket_fd, &length, sizeof (length));
/* 輸出字符串。*/
write (socket_fd, text, length);
}

int main (int argc, char* const argv[])

{
const char* const socket_name = argv[1];
const char* const message = argv[2];
int socket_fd;
struct sockaddr_un name;

/* 創建套接字。*/
socket_fd = socket (PF_LOCAL, SOCK_STREAM, 0);
/* 將服務器地址寫入套接字地址結構中。*/
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, socket_name);
/* 連接套接字。*/
connect (socket_fd, &name, SUN_LEN (&name));
/* 將由命令行指定的文本信息寫入套接字。*/
write_text (socket_fd, message);
close (socket_fd);
return 0;
}

在客戶端發送文本信息之前,客戶端先通過發送整型變量length 的方式將消息的長度
通知服務端。類似的,服務端在讀取消息之前先從套接字讀取一個整型變量以獲取消息的長
度。這提供給服務器一個在接收信息之前分配合適大小的緩沖區保存信息的方法。
要嘗試這個例子,應在一個窗口中運行服務端程序。指定一個套接字文件的路徑——例
如 /tmp/socket 作為參數:
% ./socket-server /tmp/socket

在另一個窗口中指明同一個套接字和消息,並多次運行客戶端程序。
% ./socket-client /tmp/socket “Hello, world.”
% ./socket-client /tmp/socket “This is a test.”

服務端將接收並輸出這些消息。要關閉服務端程序,從客戶端發送“quit”即可:
% ./socket-client /tmp/socket “quit”

這樣服務端程序就會退出。

5.5.6 Internet 域套接字
UNIX 域套接字只能用於同主機上的兩個進程之間通信。Internet 域套接字則可以用來
連接網絡中不同主機上的進程。
用於在Internet范圍連接不同進程的套接字屬於 Internet命名空間,使用PF_INET表示。
最常用的協議是TCP/IP。Internet 協議(Internet Protocol ,IP )是一個低層次的協議,負責
包在Internet 中的傳遞,並在需要的時候負責分片和重組數據包。它只保證“盡量”地發送,
因此包可能在傳輸過程中丟失,或者前后順序被打亂。參與傳輸的每台主機都由一個獨一無

二的IP 數字標識。傳輸控制協議(Transmission Control Protocol ,TCP )架構於 IP 協議之
上,提供了可靠的面向連接的傳輸。它允許主機之間建立類似電話的連接且保證數據傳輸的
可靠性和有序性。
Internet 套接字的地址包含兩個部分:主機和端口號。這些信息保存在sockaddr_in
結構中。將 sin_family 字段設置為AF_INET以表示這是一個Internet 命名空間地址
標主機的Internet 地址作為一個 32位整數保存在sin_addr 字段中。端口號用於區分同一台
主機上的不同套接字。因為不同主機可能將多字節的值按照不同的字節序存儲,應將htons
將端口號轉換為網絡字節序。參看ip 的手冊頁以獲取更多信息。
可以通過調用gethostbyname 函數將一個可讀的主機名——包括標准的以點分割的
IP 地址(如 10.0.0.1 )或DNS 名(如 www.codesourcery.com )——轉換為32位IP 數
字。這個函數返回一個指向struct hosten t 結構的指針;其中的h_addr 字段包含了主
機的IP 數字。參考列表5.12 中的示例程序。
列表5.12 展示了 Internet 域套接字的使用。這個程序會獲取由命令行指定的網頁服務器
的首頁。

代碼列表 5.12 (socket-inet.c)從 WWW 服務器讀取信息

#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>

/* 從服務器套接字中讀取主頁內容。返回成功的標記。*/

void get_home_page (int socket_fd)
{
char buffer[10000];
ssize_t number_characters_read;
/* 發送 HTTP GET 請求獲取主頁內容。*/
sprintf (buffer, “GET /\n”);
write (socket_fd, buffer, strlen (buffer));
/* 從套接字中讀取信息。調用 read 一次可能不會返回全部信息,所以我們必須不
斷嘗試讀取直到真正結束。*/
while (1) {
number_characters_read = read (socket_fd, buffer, 10000);
if (number_characters_read == 0)
return;
/* 將數據輸出到標准輸出流。*/
fwrite (buffer, sizeof (char), number_characters_read, stdout);
}
}

int main (int argc, char* const argv[])
{
int socket_fd;
struct sockaddr_in name;
struct hostent* hostinfo;

/* 創建套接字。*/
socket_fd = socket (PF_INET, SOCK_STREAM, 0);
/* 將服務器地址保存在套接字地址中。*/
name.sin_family = AF_INET;
/* 將包含主機名的字符串轉換為數字。*/
hostinfo = gethostbyname (argv[1]);
if (hostinfo == NULL)
return 1;
else
name.sin_addr = *((struct in_addr *) hostinfo->h_addr);
/* 網頁服務器使用 80 端口。*/
name.sin_port = htons (80);

/* 連接到網頁服務器。*/
if (connect (socket_fd, &name, sizeof (struct sockaddr_in)) == -1)
{
perror (“connect”);
return 1;
}
/* 讀取主頁內容。*/
get_home_page (socket_fd);

return 0;
}


這個程序從命令行讀取服務器的主機名(不是URL——也就是說,地址中不包括
“http://”部分)。它通過調用gethostbyname 將主機名轉換為IP 地址,然后與主機的
80端口建立一個流式(TCP 協議的)套接字。網頁服務器通過超文本傳輸協議(HyperText
Transport Protocol ,HTTP),因此程序發送 HTTP GET命令給服務器,而服務器傳回主頁
內容作為響應。
例如,可以這樣運行程序從www.codesourcery.com 獲取主頁:
% ./socket-inet www.codesourcery.com
<html>
<meta http-equiv="Content-Type" content="text/html;
charset=iso-8859-1">
..

標准端口號
根據習慣,網頁服務器在 80 端口監聽客戶端連接。多數 Internet 網絡服務都被分配
了標准端口號。例如,使用 SSL 的安全網頁服務器的在443 端口監聽連接,而郵件服務
器(利用SMTP 協議通信)使用端口25 。
在GNU/Linux 系統中,協議——服務名關系列表被保存在了/etc/services 。
該文件的第一欄是協議或服務名,第二欄列舉了對應的端口號和連接類型:tcp 代表
了面向連接的協議,而udp 代表數據報類型的。
如果你用Internet 域套接字實現了一個自己的協議,應使用高於 1024 的端口號進
行監聽。
5.5.7 套接字對
如前所示,pipe 函數創建了兩個文件描述符,分別代表管道的兩端。管道有所限制因
為文件描述符只能被相關進程使用且經由管道進行的通信是單向的。而socketpair 函數
為一台主機上的一對相連接的的套接字創建兩個文件描述符。這對文件描述符允許相關進程
之間進行雙向通信。
它的前三個參數與socket 系統調用相同:分別指明了域、通信類型(譯者著:原文為
connection style 連接類型,與前文不符,特此修改)和協議。最后一個參數是一個包
含兩個元素的整型數組,用於保存創建的兩個套接字的文件描述符,與pipe 的參數類似。
當調用socketpair 的時候,必須指定PF_LOCAL作為域。

 

 


免責聲明!

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



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