前面說到的程序間的通信,所通信的程序都是在同一台計算機上的,而使用socket進行通信的程序可以是同一台計算機的程序,也是可以是通過網絡連接配接起來的不同計算機上的程序。通常我們使用socket進行網絡程式設計,這裡将會簡單地講述如何使用socket進行簡單的網絡程式設計。
一、什麼是socket
socket,即套接字是一種通信機制,憑借這種機制,客戶/伺服器(即要進行通信的程序)系統的開發工作既可以在本地單機上進行,也可以跨網絡進行。也就是說它可以讓不在同一台計算機但通過網絡連接配接計算機上的程序進行通信。也因為這樣,套接字明确地将用戶端和伺服器區分開來。
二、套接字的屬性
套接字的特性由3個屬性确定,它們分别是:域、類型和協定。
1、套接字的域
它指定套接字通信中使用的網絡媒體,最常見的套接字域是AF_INET,它指的是Internet網絡。當客戶使用套接字進行跨網絡的連接配接時,它就需要用到伺服器計算機的IP位址和端口來指定一台聯網機器上的某個特定服務,是以在使用socket作為通信的終點,伺服器應用程式必須在開始通信之前綁定一個端口,伺服器在指定的端口等待客戶的連接配接。另一個域AF_UNIX表示UNIX檔案系統,它就是檔案輸入/輸出,而它的位址就是檔案名。
2、套接字類型
網際網路提供了兩種通信機制:流(stream)和資料報(datagram),因而套接字的類型也就分為流套接字和資料報套接字。這裡主要講流套接字。
流套接字由類型SOCK_STREAM指定,它們是在AF_INET域中通過TCP/IP連接配接實作,同時也是AF_UNIX中常用的套接字類型。流套接字提供的是一個有序、可靠、雙向位元組流的連接配接,是以發送的資料可以確定不會丢失、重複或亂序到達,而且它還有一定的出錯後重新發送的機制。
與流套接字相對的是由類型SOCK_DGRAM指定的資料報套接字,它不需要建立連接配接和維持一個連接配接,它們在AF_INET中通常是通過UDP/IP協定實作的。它對可以發送的資料的長度有限制,資料報作為一個單獨的網絡消息被傳輸,它可能會丢失、複制或錯亂到達,UDP不是一個可靠的協定,但是它的速度比較高,因為它并一需要總是要建立和維持一個連接配接。
3、套接字協定
隻要底層的傳輸機制允許不止一個協定來提供要求的套接字類型,我們就可以為套接字選擇一個特定的協定。通常隻需要使用預設值。
三、套接字位址
每個套接字都有其自己的位址格式,對于AF_UNIX域套接字來說,它的位址由結構sockaddr_un來描述,該結構定義在頭檔案sys/un.h中,它的定義如下:
struct sockaddr_un{
sa_family_t sun_family;//AF_UNIX,它是一個短整型
char sum_path[];//路徑名
};
對于AF_INET域套接字來說,它的位址結構由sockaddr_in來描述,它至少包括以下幾個成員:
struct sockaddr_in{
short int sin_family;//AF_INET
unsigned short int sin_port;//端口号
struct in_addr sin_addr;//IP位址
而in_addr被定義為:
struct in_addr{
unsigned long int s_addr;
四、基于流套接字的客戶/伺服器的工作流程
使用socket進行程序通信的程序采用的客戶/伺服器系統是如何工作的呢?
1、伺服器端
首先伺服器應用程式用系統調用socket來建立一個套接安,它是系統配置設定給該伺服器程序的類似檔案描述符的資源,它不能與其他的程序共享。
接下來,伺服器程序會給套接字起個名字,我們使用系統調用bind來給套接字命名。然後伺服器程序就開始等待客戶連接配接到這個套接字。
然後,系統調用listen來建立一個隊列并将其用于存放來自客戶的進入連接配接。
最後,伺服器通過系統調用accept來接受客戶的連接配接。它會建立一個與原有的命名套接不同的新套接字,這個套接字隻用于與這個特定用戶端進行通信,而命名套接字(即原先的套接字)則被保留下來繼續處理來自其他客戶的連接配接。
2、用戶端
基于socket的用戶端比伺服器端簡單,同樣,客戶應用程式首先調用socket來建立一個未命名的套接字,然後将伺服器的命名套接字作為一個位址來調用connect與伺服器建立連接配接。
一旦連接配接建立,我們就可以像使用底層的檔案描述符那樣用套接字來實作雙向資料的通信。
五、流式socket的接口及作用
socket的接口函數聲明在頭檔案sys/types.h和sys/socket.h中。
1、建立套接字——socket系統調用
該函數用來建立一個套接字,并傳回一個描述符,該描述符可以用來通路該套接字,它的原型如下:
int socket(int domain, int type, int protocol);
函數中的三個參數分别對應前面所說的三個套接字屬性。protocol參數設定為0表示使用預設協定。
2、命名(綁定)套接字——bind系統調用
該函數把通過socket調用建立的套接字命名,進而讓它可以被其他程序使用。對于AF_UNIX,調用該函數後套接字就會關聯到一個檔案系統路徑名,對于AF_INET,則會關聯到一個IP端口号。函數原型如下:
int bind( int socket, const struct sockaddr *address, size_t address_len);
成功時傳回0,失敗時傳回-1;
3、建立套接字隊列(監聽)——listen系統調用
該函數用來建立一個隊列來儲存未處理的請求。成功時傳回0,失敗時傳回-1,其原型如下:
int listen(int socket, int backlog);
backlog用于指定隊列的長度,等待處理的進入連接配接的個數最多不能超過這個數字,否則往後的連接配接将被拒絕,導緻客戶的連接配接請求失敗。調用後,程式一直會監聽這個IP端口,如果有連接配接請求,就把它加入到這個隊列中。
4、接受連接配接——accept系統調用
該系統調用用來等待客戶建立對該套接字的連接配接。accept系統調用隻有當客戶程式試圖連接配接到由socket參數指定的套接字上時才傳回,也就是說,如果套接字隊列中沒有未處理的連接配接,accept将阻塞直到有客戶建立連接配接為止。accept函數将建立一個新套接字來與該客戶進行通信,并且傳回新套接字的描述符,新套接字的類型和伺服器監聽套接字類型是一樣的。它的原型如下:
int accept(int socket, struct sockaddr *address, size_t *address_len);
address為連接配接用戶端的位址,參數address_len指定客戶結構的長度,如果客戶位址的長度超過這個值,它将會截斷。
5、請求連接配接——connect系統調用
該系統調用用來讓客戶程式通過在一個未命名套接字和伺服器監聽套接字之間建立連接配接的方法來連接配接到伺服器。它的原型如下:
int connect(int socket, const struct sockaddr *address, size_t address_len);
參數socket指定的套接字連接配接到參數addres指定的伺服器套接字。成功時傳回0,失敗時傳回-1.
6、關閉socket——close系統調用
該系統調用用來終止伺服器和客戶上的套接字連接配接,我們應該總是在連接配接的兩端(伺服器和客戶)關閉套接字。
六、程序使用流式socket進行通信
下面用多個客戶程式和一個伺服器程式來展示程序間如何利用套接字進行通信。
sockserver.c是一個伺服器程式,它首先建立套接字,然後綁定一個端口再監聽套接字,忽略子程序的停止消息等,然後它進入循環,一直循環檢查是否有客戶連接配接到伺服器,如果有,則調用fork建立一個子程序來處理請求。利用read系統調用來讀取用戶端發來的資訊,利用write系統調用來向用戶端發送資訊。這個伺服器的工作非常簡單,就是把客戶發過來的字元+1,再發送回給客戶。
sockclient.c是一個客戶程式,它同樣要先建立套接,然後連接配接到指定IP端口伺服器,如果連接配接成功,就用write來發送資訊給伺服器,再用read擷取伺服器處理後的資訊,再輸出。
伺服器sockserver.c的源代碼如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int server_sockfd = -1;
int client_sockfd = -1;
int client_len = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//建立流套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
<span style="white-space:pre"> </span>//設定伺服器接收的連接配接位址和監聽的端口
server_addr.sin_family = AF_INET;//指定網絡套接字
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//接受所有IP位址的連接配接
server_addr.sin_port = htons(9736);//綁定到9736端口
//綁定(命名)套接字
bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
//建立套接字隊列,監聽套接字
listen(server_sockfd, 5);
//忽略子程序停止或退出信号
signal(SIGCHLD, SIG_IGN);
while(1)
{
char ch = '\0';
client_len = sizeof(client_addr);
printf("Server waiting\n");
//接受連接配接,建立新的套接字
client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
if(fork() == 0)
{
//子程序中,讀取用戶端發過來的資訊,處理資訊,再發送給用戶端
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
else
//父程序中,關閉套接字
}
}
客戶sockclient.c的源代碼如下:
#include <arpa/inet.h>
int sockfd = -1;
int len = 0;
struct sockaddr_in address;
int result;
char ch = 'A';
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//設定要連接配接的伺服器的資訊
address.sin_family = AF_INET;//使用網絡套接字
address.sin_addr.s_addr = inet_addr("127.0.0.1");//伺服器位址
address.sin_port = htons(9736);//伺服器所監聽的端口
len = sizeof(address);
//連接配接到伺服器
result = connect(sockfd, (struct sockaddr*)&address, len);
if(result == -1)
perror("ops:client\n");
exit(1);
//發送請求給伺服器
write(sockfd, &ch, 1);
//從伺服器擷取資料
read(sockfd, &ch, 1);
printf("char form server = %c\n", ch);
close(sockfd);
exit(0);
運作結果如下:

在本例子中,我們啟動了一個伺服器程式和三個客戶程式,從運作的結果來看,用戶端發送給伺服器程式的所有請求都得到了處理,即把A變成了B。對于伺服器和客戶程式之間使用的read和write系統調用跟使用命名管道時阻塞的read、write系統調用一樣。例如客戶程式調用read時,如果伺服器程式沒有向指定的客戶程式的socket中寫入資訊,則read調用會一直阻塞。
七、流式套接字給我印象
給我的感覺是流式套接字很像命名管道,但是它卻可以使不在同一台計算機而通過網絡連接配接的不同計算機上的程序進行通信,功能真是非常的強大。