linux程序設計(套接字)+TCP/IP網絡編程學習筆記
什麼是套接字?
應用層通過傳輸層進行數據通信時,TCP和UDP會遇到同時爲多個應用程序進程提供併發服務的問題。多個TCP連接或多個應用程序進程可能需要通過同一個TCP協議端口傳輸數據。爲了區別不同的應用程序進程和連接,許多計算機操作系統爲應用程序與TCP/IP協議交互提供了稱爲套接字(Socket)的接口,區分不同應用程序進程間的網絡通信和連接。
網絡化的應用程序在開始任何通訊之前都必需要創建套接字。就像電話的插口一樣,沒有它就完全沒辦法通信。
生成套接字,主要有3個參數:通信的目的IP地址、使用的傳輸層協議(TCP或UDP)和使用的端口號。
Socket可以看成在兩個程序進行通訊連接中的一個端點,一個程序將一段信息寫入Socket中,該Socket將這段信息發送給另外一個Socket中,使這段信息能傳送到其他程序中.
Host A上的程序A將一段信息寫入Socket中,Socket的內容被Host A的網絡管理軟件訪問,並將這段信息通過Host A的網絡接口卡發送到Host B,Host B的網絡接口卡接收到這段信息後,傳送給Host B的網絡管理軟件,網絡管理軟件將這段信息保存在Host B的Socket中,然後程序B才能在Socket中閱讀這段信息。
通過套接字接口可以實現網絡間的進程通信.
端口號就是在同一操作系統內爲區分不同套接字而設置的,因此無法將1個端口號分配給不同的套接字.雖然端口號不能重複,但TCP套接字和UDP套接字不會公用端口號,所以允許重複.
套接字是一種通信機制,這使得客戶/服務器系統的開發工作即可以在本地單機上進行,也可以跨網絡進行.linux所提供的功能(如打印服務,連接數據庫和提供web頁面)和網絡工具(如用於遠程登錄的rlogin和用於文件傳輸的ftp)通常都是通過套接字來進行通信的.
套接字明確的將客戶和服務器區分開來,這與管道是有區別的.套接字機制可以實現將多個客戶連接到一個服務器.
基於Linux的文件操作
對於Linux而言,socket操作與文件操作沒有區別,socket被認爲是文件的一種,因此在網絡數據傳輸的過程中可以使用文件I/O的相關函數.
文件描述符是系統分配給文件或套接字的整數
分配給標準輸入輸出及標準錯誤的文件描述符:
文件描述符 | 對象 |
0 | 標準輸入:Standard Input |
1 | 標準輸出:Standard Output |
2 | 標準錯誤:Standard Error |
1.打開文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path,int flag);
成功時返回文件描述符,失敗返回-1
- path 文件名的字符串地址
- flag 文件打開模式信息(如需傳遞多個參數,則應通過位或運算(OR)符組合並傳遞)
文件打開模式:
打開模式 | 含義 |
O_CREAT | 必要時創建文件 |
O_TRUNC | 刪除全部現有數據 |
O_APPEND | 維持現有數據,保存到其後面 |
O_RDONLY | 只讀打開 |
O_WRONLY | 只寫打開 |
O_RDWR | 讀寫打開 |
2.關閉文件
#include <unistd.h>
int close(int fd);
成功時返回0,失敗返回-1
- fd 需要關閉的文件或套接字的文件描述符
3. 將數據寫入文件
write函數用於向文件輸出(傳輸)數據.Linux系統中不區分文件與套接字,通過套接字向其他計算機傳遞數據時也用write函數
#include <unistd.h>
ssize_t wirte(int fd,const void *buf,size_t nbytes);
成功時返回寫入的字節數,失敗返回-1
- fd 顯示數據傳輸對象的文件描述符
- buf 保存要傳輸數據的緩衝地址值
- nbytes 要傳輸數據的字節數
size_t是通過typedef聲明的unsigned int類型.對ssize_t來說,size_t前面多加的s代表signed,即ssize_t是通過typedef聲明的signed int類型
4.讀取文件中的數據
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t nbytes);
成功時返回接收的字節數(但遇到文件結尾則返回0),失敗時返回-1
- fd 顯示數據接收對象的文件描述符
- buf 要保存接收數據的緩衝地址值
- nbytes 要接收數據的最大字節數
文件描述符與套接字
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main()
{
int fd1,fd2,fd3;
fd1=socket(PF_INET,SOCK_STREAM,0);
fd2=open("test.dat",O_CREAT|O_WRONLY|O_TRUNC);
fd3=socket(PF_INET,SOCK_STREAM,0);
printf("file descriptor 1: %d\n",fd1);
printf("file descriptor 2: %d\n",fd2);
printf("file descriptor 3: %d\n",fd3);
close(fd1);
close(fd2);
close(fd3);
return 0;
}
//從輸出的文件描述符整數值可以看出,描述符從3開始以由大到小的順序編號,因爲0,1,2是分配給標準I/O的文件描述符
套接字連接
理解套接字應用程序如何通過套接字維持一個連接?
1. 服務器應用程序用系統調用socket來創建一個套接字,它是系統分配給該服務器進程的類似文件描述符的資源,它不能與其他進程共享.
2. 服務器進程給創建的套接字分配一個名字.本地套接字的名字是linux文件系統中的文件名,一般放在/tmp或/usr/tmp目錄中.網絡套接字的名字是與客戶連接的特定網絡有關的服務標識符(端口號或訪問點).該標識符允許linux將進入的針對特定端口號的連接轉到正確的服務器進程.
- 系統調用bind給套接字命名,然後服務器進程就開始等待客戶連接到這個命名套接字.
- 系統調用listen創建一個用於存放來自客戶的進入連接的隊列
- 服務器通過系統調用accept來接受客戶的連接
3 服務器調用accept時,會創建一個與原有的命名套接字不同的新套接字.新建套接字只用於與這個特定的客戶進行通信,而命名套接字被保留下來繼續處理來自其他客戶的連接.服務器可以同時接受多個連接.對於一個簡單的服務器,後續的客戶將在監聽隊列中等待,直到服務器再次準備就緒.
4. 基於套接字系統的客戶端更加簡單.客戶首先調用socket創建一個未命名套接字,然後將服務器的命名套接字作爲一個地址來調用connect與服務器建立連接.
5. 一旦連接建立,就可以像使用底層文件描述符那樣用套接字來實現雙向的數據通信.
套接字屬性
3個屬性決定套接字的特性:域(domain),類型(type),以及協議(protocol).套接字還用地址作爲它的名字.地址的格式隨域(又被稱爲協議族,protocol family)的不同而不同.每個協議族又可以使用一個或多個地址族來定義地址格式.(每種地址族適用的地址族均不同)
1. 套接字的域(域又稱爲協議族)
域指定套接字通信中使用的網絡介質.最常見的套接字域是AF_INET,它指的是Internet網絡.其底層的協議–網際協議(IP)只有一個地址族,它使用IP地址來指定網絡中的計算機.
客戶通過IP端口指定一臺聯網機器上的某個特定的服務.系統內部,使用一個唯一的16位整數來標識端口;系統外部,需要通過IP地址和端口號的組合來確定.套接字作爲通信的終點,它必須在開始通信之前綁定一個端口.
服務器在特定的端口等待客戶的連接.標準服務對應知名端口號(標準端口號).本地服務可以使用非標準的端口地址.
UNIX文件系統域AF_UNIX,一臺位聯網的計算機上的套接字也可以使用該域.這個域的底層協議就是文件輸入/輸出,而它的地址就是文件名.
頭文件sys/socket.h中聲明的協議族
名稱 | 協議族 |
PF_INET | IPv4互聯網協議族 |
PF_INET6 | IPv6互聯網協議族 |
PF_LOCAL | 本地通信的UNIX協議族 |
PF_PACKET | 底層套接字的協議族 |
PF_IPX | IPX Novell協議族 |
套接字中實際採用的最終協議信息是通過socket函數的第三個參數傳遞的.在指定的協議族範圍內通過第一個參數決定第三個參數.
2. 套接字類型
套接字類型指的是套接字的數據傳輸方式,該類型決定了創建的套接字的數據傳輸方式.
一個套接字域可能有多種不同的通信方式,而每種通信方式又有其不同的特性.但AF_UNIX域的套接字提供了一個可靠的雙向通信路徑.
因特網協議提供了兩種通信機制:流(stream)和數據包(datagram).
面向連接的套接字(SOCK_STREAM)
流套接字(在某些方面類似與標準的輸入/輸出流)提供的是一個有序,可靠,雙向字節流的連接.因此發送的數據可以確保不會丟失,複製或亂序到達,並且在這一過程中發生的錯誤也不會顯示出來.大的消息將被分片,傳輸,再重組.這很像一個文件流,它接收大量的數據,然後以小數據塊的形式將它們寫入底層磁盤.流套接字的行爲是可預見的.
由類型SOCK_STREAM指定流套接字,它們是在AF_INET域中通過TCP/IP連接實現的.它們也是AF_UNIX域中常用的套接字類型.
TCP/IP代表的是傳輸控制協議(Transmission Control Protocol)/網際協議(Internet Protocol).IP協議是針對數據包的底層協議,它提供從一臺計算機通過網絡到達另一臺計算機的路由.TCP協議提供排序,流控和重傳,以確保大數據的傳輸可以完整的到達目的地或報告一個適當的錯誤條件.
SOCK_STREAM特徵:
- 傳輸過程中數據不會消失
- 按序傳輸數據
- 傳輸的數據不存在數據邊界
收發數據的套接字內部有緩衝(buffer),即字節數組.通過套接字傳輸的數據將保存到該數組.因此,收數據並不意味着馬上調用read函數,只要不超過數組容量,則有可能在數據填充滿緩衝後通過1次read函數調用讀取全部,也有可能分成多次read函數調用進行讀取.
面向消息的套接字(SOCK_DGRAM)
由類型SOCK_DGRAM指定的數據報套接字不建立和維持一個連接.它對可以發送的數據報的長度有限制.數據報作爲一個單獨的網絡消息被傳輸,它可能會丟失,複製或亂序到達.
數據報套接字是在AF_INET域中通過UDP/IP連接實現的,它提供的是一種無序的不可靠服務.優點:開銷較小,速度快.
數據報適用於信息服務中的”單次”(single-shot)查詢,它主要用來提供日常狀態信息或執行低優先級的日誌記錄.它的優點是服務器的崩潰不會給客戶端造成不便,也不會要求客戶重啓,因爲基於數據報的服務器通常不保留連接信息,所以它們可以在不打擾其客戶的前提下停止並重啓.
SOCK_DGRAM特徵:
- 強調快速傳輸而非傳輸順序
- 傳輸的數據可能丟失也可能損毀
- 傳輸的數據有數據邊界
- 限制每次傳輸的數據的大小
- 存在數據邊界(意味着接收數據的次數應和傳輸次數相同)
套接字協議
只要底層的傳輸機制允許不止一個協議來提供要求的套接字類型,我們就可以爲套接字選擇一個特定的協議.(UNIX網絡套接字和文件系統套接字)
socket函數的前兩個參數傳遞了協議族信息和套接字數據傳輸方式,大部分情況下可以向第三個參數傳遞0,除非遇到下面的情況:
同一協議族中存在多個數據傳輸方式相同的協議
數據傳輸方式相同,但協議不同,此時需要通過第三個參數具體指定協議信息.
- IPPROTO_TCP “IPv4協議族中面向連接的套接字”
- IPPROTO_UDP “IPv4協議族中面向消息的套接字”
TCP服務器端的默認函數調用順序
- socket()創建套接字
- bind()分配套接字地址
- listen()等待連接請求狀態
- accept()允許連接
- read()/write()數據交換
- close()斷開連接
TCP客戶端的默認函數調用順序
- socket()創建套接字
- connect()請求連接
- read()/write()交換數據
- close()斷開連接
基於TCP/IP的服務器端/客戶端函數調用關係
整體流程如下:服務器端創建套接字後連續調用bind,listen函數進入等待狀態,客戶端通過調用connect函數發起連接請求.客戶端只能等到服務器端調用listen函數後才能調connect函數.客戶端調用connect函數前,服務器端可能先調用accept函數,此時服務器端在調用accept函數時進入阻塞狀態,直到客戶端調用connect函數爲止.
創建套接字
socket系統調用創建一個套接字並返回一個描述符,該描述符可以用來訪問該套接字.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
成功時返回文件描述符,失敗時返回-1
創建的套接字是一條通信線路的一個端點.
- domain指定協議族(套接字中使用的協議族信息)
- type參數指定這個套接字的通信類型(套接字數據傳輸類型)
- protocol參數指定使用的協議(計算機間通信中使用的協議信息)
每種協議族適用的地址族均不同,如IPv4使用4字節地址族,IPv6使用16字節地址族.
關於PF_INET和AF_INET的區別?
在寫網絡程序的時候,建立TCP socket:
sock = socket(PF_INET, SOCK_STREAM, 0);
然後在綁定本地地址或連接遠程地址時需要初始化sockaddr_in結構,其中指定address family時一般設置爲AF_INET,即使用IP。
相關頭文件中的定義:
AF = Address Family
PF = Protocol Family
AF_INET = PF_INET
在windows中的Winsock2.h中,
#define AF_INET 0
#define PF_INET AF_INET
所以在windows中AF_INET與PF_INET完全一樣.
而在Unix/Linux系統中,在不同的版本中這兩者有微小差別.對於BSD,是AF,對於POSIX是PF.
建議:對於socket的domain參數,使用PF_LOCAL系列,
而在初始化套接口地址結構時,則使用AF_LOCAL.
常用地址族:
域 | 說明 |
AF_UNIX | UNIX域協議(文件系統套接字) |
AF_INET | ARPR因特網協議(UNIX網絡套接字)(/td) |
AF_ISO | ISO標準協議 |
AF_NS | 施樂(Xerox)網絡系統協議 |
AF_IPX | Novell IPX協議 |
AF_APPLETALK | Appletalk DDS |
最常用的套接字域是AF_UNIX和AF_INET,AF_UNIX用於通過UNIX和Linux文件系統實現的本地套接字,AF_INET用於UNIX網絡套接字.AF_INET套接字可以用於通過包括因特網在內的TCP/IP網絡進行通信的程序.微軟Windows系統的Winsock接口也提供了對這個套接字域的訪問功能.
type參數指定用於新套接字的通信特性.它的取值包括SOCK_STREAM和SOCK_DGRAM.
- SOCK_STREAM是一個有序,可靠,面向連接的雙向字節流.對於AF_INET域的套接字,它默認是通過一個TCP連接來提供這一特性,TCP連接在兩個流套接字端點之間建立.數據可以通過套接字連接進行雙向傳遞.TCP協議所提供的機制可以用於分片和重組長消息,並且可以重傳可能在網絡中丟失的數據.
- SOCK_DGRAM是數據報服務.可以用它發送最大長度固定(通常比較小)的消息,但消息是否會被正確傳遞或消息是否不會亂序到達並沒有保證.對於AF_INET域套接字來說,這種類型的通信是由UDP數據報來提供的.
一般由套接字類型和套接字域決定通信所用協議,通常將protocol設置爲0來表示使用默認協議.
socket系統調用返回一個描述符,它在許多方面都類似於底層的文件描述符.當連接到另一端的套接字後,就可以用read和write系統調用了,通過這個描述符來在套接字上發送和接收數據.close系統調用用於結束套接字連接.
套接字地址
POSIX是爲UNIX系列操作系統設立的標準,它定義了一些數據類型,如表:
數據類型名稱 | 數據類型說明 | 聲明的頭文件 |
int8_t | singed 8-bit int | sys/types.h |
uint8_t | unsinged 8-bit int(unsigned char) | sys/types.h |
int16_t | singed 16-bit int | sys/types.h |
uint16_t | unsinged 16-bit int(unsigned short) | sys/types.h |
int32_t | singed 32-bit int | sys/types.h |
uint32_t | unsinged 32-bit int(unsigned long) | sys/types.h |
sa_family_t | 地址族(address family) | sys/socket.h |
socklen_t | 長度(length of struct) | sys/socket.h |
in_addr_t | IP地址,聲明爲uint32_t | netinet/in.h |
in_port_t | 端口號,聲明爲uint16_t | netinet/in.h |
每個套接字域都有其地址格式.對於AF_UNIX域套接字,它的地址由結構sockaddr_un描述,該結構定義在頭文件sys/un.h中(文件系統套接字)
struct sockaddr_un{
sa_family_t sun_family; /*AF_UNIX*/
char sun_path[]; /*pathname*/
};
對套接字進行處理的系統調用也許會接受不同類型的地址,每種地址格式都使用一種類似的結構描述,它們都以一個指定地址類型(套接字域)的成員sun_family開始.在AF_UNIX域中,套接字地址由結構中的sun_path成員中的文件名指定.
在當前的Linux系統中,由X/Open規範定義的類型sa_family_t在頭文件sys/un.h中聲明,它是短整數類型.sun_path指定的路徑名長度是有限制的(Linux規定的是108個字符,其他系統可能使用的是更清楚的常量,如UNIX_MAX_PATH).因爲地址結構的長度不一致,所以許多套接字調用需要用到一個用來複制特定地址結構的長度變量或將它作爲一個輸出.
//server
//創建一個服務器套接字,將它綁定到一個名字,然後創建一個監聽隊列,開始接受客戶的連接.
//包含必要的頭文件並設置變量:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;
//刪除以前的套接字,爲服務器創建一個未命名的套接字
unlink("server_socket");
server_sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//命令套接字
server_address.sun_family=AF_UNIX;
strcpy(server_address.sun_path,"server_socket");
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
//創建一個連接隊列,開始等待客戶進行連接
listen(server_sockfd,5);
while(1)
{
char ch;
printf("server waiting\n");
//接受一個連接:
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
//對client_sockfd套接字上的客戶進行讀寫操作
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
//client
//包含一些必要的頭文件並設置變量
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch='A';
//爲客戶創建一個套接字
sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//根據服務器的情況給套接字命令
address.sun_family=AF_UNIX;
strcpy(address.sun_path,"server_socket");
len=sizeof(address);
//將我們的套接字連接到服務器的套接字:
result=connect(sockfd,(struct sockaddr *)&address,len);
if(result == -1)
{
perror("oops:client1");
exit(1);
}
//現在就可以通過sockfd進行讀寫操作了
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
close(sockfd);
exit(0);
}
在AF_INET域中,套接字地址由定義在netinet/in.h中的結構sockaddr_in來指定:(網絡套接字),此結構體作爲地址信息傳遞給bind函數.
struct sockaddr_in{
short int sin_family; /*AF_INET*/
unsigned short int sin_port; /*Port number*/
struct in_addr sin_addr;/*Internet address*/
};
IP地址結構in_addr被定義爲:
struct in_addr{
unsigned long int s_addr;
}
TCP/IP網絡原理中的定義如下:
struct sockaddr_in
{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口號
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
};
struct in_addr
{
in_addr_t s_addr; //32位IPv4地址
}
- sin_family
每種協議族適用的地址族均不同.如IPv4使用4字節地址族,IPv6使用16字節地址族. - sin_port
以網絡字節序保存16位端口號 - sin_addr
以網絡字節序保存32位IP地址信息.
struct sockaddr
{
short int sin_family; //地址族
char sa_data[14]; //地址信息
}
此結構體成員sa_data保存的地址信息中需包含IP地址和端口號,剩餘部分應填充0.結構體sockaddr並非只爲IPv4設計.
struct sockaddr是通用的套接字地址,而struct sockaddr_in則是internet環境下套接字的地址形式,二者長度一樣都是16字節,二者是並列結構,指向sockaddr_in結構的指針也可以指向sockaddr.一般情況下需要把sockaddr_in結構強制轉換成sockaddr結構再次傳入系統調用函數中.
//server
//創建一個服務器套接字,將它綁定到一個名字,然後創建一個監聽隊列,開始接受客戶的連接.
//包含必要的頭文件並設置變量:
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
//#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int server_sockfd,client_sockfd;
int server_len,client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
//刪除以前的套接字,爲服務器創建一個未命名的套接字
//unlink("server_socket");
server_sockfd=socket(AF_INET,SOCK_STREAM,0);
//命令套接字
server_address.sin_family=AF_INET;
//strcpy(server_address.sun_path,"server_socket");
server_address.sin_addr.s_addr=inet_addr("127.0.0.1");
server_address.sin_port=9734;
server_len=sizeof(server_address);
bind(server_sockfd,(struct sockaddr *)&server_address,server_len);
//創建一個連接隊列,開始等待客戶進行連接
listen(server_sockfd,5);
while(1)
{
char ch;
printf("server waiting\n");
//接受一個連接:
client_len=sizeof(client_address);
client_sockfd=accept(server_sockfd,(struct sockaddr *)&client_address,&client_len);
//對client_sockfd套接字上的客戶進行讀寫操作
read(client_sockfd,&ch,1);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
/*
想允許服務器和遠程客戶進行通信,就必須指定一組你允許連接的IP地址.可以使用特殊值INADDR_ANY,
來表示,你將接受來自計算機任何網絡接口的連接
當服務端監聽INADDR_ANY時,
1. 同一電腦上的客戶端去connect 127.0.0.1, 是可以連接上的。
2. 或者客戶端去connect 10.251.234.120(服務器), 也是可以連接上的, 此時, 不一定要求客戶端和服務端處在同一臺電腦上
*/
//client
//包含必要的頭文件並設置變量
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int sockfd;
int len;
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=9734;
len=sizeof(address);
//將我們的套接字連接到服務器的套接字:
result=connect(sockfd,(struct sockaddr *)&address,len);
if(result == -1)
{
perror("oops:client1");
exit(1);
}
//現在就可以通過sockfd進行讀寫操作了
write(sockfd,&ch,1);
read(sockfd,&ch,1);
printf("char from server = %c\n",ch);
close(sockfd);
close(sockfd);
exit(0);
}
命名套接字
只有命名的套接字,纔可以被其他進程使用.這樣,AF_UNIX套接字就會關聯到一個文件系統的路徑名.
#include <sys/socket.h>
int bind(int socket,const struct sockaddr *address,size_t address_len);
bind調用成功時返回0,失敗返回-1
- socket 要分配地址信息的套接字文件描述符
- address 存有地址信息的結構體變量地址值
- address_len 第二個結構體變量的長度
bind系統調用把參數address中的地址分配給與文件描述符socket關聯的未命名套接字.參數address_len參數傳遞地址結構的長度.
地址長度和格式取決於地址族.bind調用需要將一個特定的地址結構指針轉換爲指向通用地址類型(struct sockaddr *)
創建套接字隊列
listen系統調用創建一個隊列來保存未處理的請求,爲了能夠在套接字上接受進入的連接.只有調用了listen函數,客戶端才能進入可發出連接請求的狀態,這時客戶端才能調用connect函數.
#include<sys/socket.h>
int listen(int socket,int backlog);
listen成功時返回0,失敗返回-1
- socket 希望進入等待連接請求狀態的套接字文件描述符,傳遞的描述符套接字參數成爲服務器端套接字(監聽套接字)
- backlog 連接請求隊列的長度,若爲5,則隊列長度爲5,表示最多使5個連接請求進入隊列.
服務器端處於等待連接請求狀態指客戶端請求連接時,受理連接前一直使請求處於等待狀態.
客戶端連接請求本身也是從網絡中接收到的一種數據,需要套接字接收.此時的服務器段套接字是接收連接請求的一名門衛.
linux系統可能會對隊列中可以容納的未處理連接的最大數目做出限制.因此,listen函數將隊列長度設置爲backlog參數值.在套接字隊列中,等待處理的進入連接的個數最多不能超過這個數字.超出的連接會被拒絕,導致客戶的連接請求失敗.listen函數的這種機制允許當服務器程序正忙於處理前一個客戶請求的時候,將後續的客戶連接放入隊列等待處理,backlog參數常用的值爲5.
受理客戶端連接請求
調用listen函數後,若有新的連接請求,則應按序受理.受理請求意味着進入可接受數據的狀態.
服務器程序創建並命名套接字之後,就可以通過accept系統調用來等待客戶建立對該套接字的連接.
#include<sys/socket.h>
int accept(int socket,struct sockaddr *address,size_t *address_len);
成功時返回創建的套接字文件描述符,失敗時返回-1
- socket服務器套接字的文件描述符
- address 保存發起連接請求的客戶端地址信息的變量地址值,調用函數後向傳遞來的地址變量參數填充客戶端地址信息
- address_len 第二個參數addr結構體的長度,但是是存有長度的變量地址.函數調用完成後,該變量即被填入客戶端地址長度
accept函數受理連接請求等待隊列中待處理的客戶端連接請求.函數調用成功時,accept函數內部將產生用於數據I/O的套接字,並返回文件描述符.套接字是自動創建的,並自動與發起連接請求的客戶端建立連接.
accept系統調用只有當有客戶程序試圖連接到由socket參數指定的套接字上時才返回.客戶程序指套接字隊列中排在第一個的未處理連接.accept函數將創建一個新套接字來與該客戶進行通信,並且返回新套接字的描述符.新套接字的類型和服務器監聽套接字類型是一樣的.
套接字必須由bind調用命名,listen調用分配一個連接隊列.連接客戶的地址將被放入address參數指向的sockaddr結構中.也可以將address參數指定爲空指針.
address_len指定客戶結構的長度.如果客戶地址的長度超過這個值,它將被截斷.
如果套接字隊列中沒有爲未處理的連接,accept將阻塞(程序將暫停)直到有客戶連接爲止.
請求連接
服務器端調用listen函數後創建連接請求等待隊列,之後客戶端即可請求連接.
客戶程序通過在一個未命名套接字和服務器監聽套接字之間建立連接的方法連接到服務器.通過connect調用完成
#include<sys/socket.h>
int connect(int socket,const struct sockaddr *address,size_t address_len);
connect調用成功時返回0,失敗返回-1
- socket 客戶端套接字文件描述符
- address 保存目標服務器端地址信息的變量地址值
- address_len 以字節爲單位傳遞已傳遞給第二個結構體參數adderss的地址變量長度.
socket指定的套接字將連接到參數address指定的服務器套接字,address指向的結構的長度由參數address_len指定.參數socket指定的套接字必須是通過socket調用獲得的一個有效的文件描述符.
客戶端調用connect函數後,發生一下情況之一纔會返回(完成函數調用)
- 服務器端接收連接請求
- 發生斷網等異常情況而中斷連接請求
注意,所謂的”接收連接”,並不意味着服務器端調用accept函數,其實是服務器段把連接請求信息記錄到等待隊列.因此connect函數返回後並不立即進行數據交換.
如果連接不能立刻建立,connect調用將阻塞一段不確定的超時時間.一旦這個超時時間到達,連接將被放棄,connect調用失敗.但如果connect調用被一個信號中斷,而該信號又得到了處理.connect調用還是會失敗,但連接嘗試並不會被放棄,而是以異步方式繼續建立,程序必須在此後進行檢查以查看連接是否成功建立.
關閉套接字
close函數可以用來終止服務器和客戶上的套接字連接,就如同關閉底層文件描述符.應該在連接的兩端都關閉套接字.服務器端,應該在read調用返回0時關閉套接字.
套接字通信
網絡套接字與文件系統套接字
套接字有本地套接字和網絡套接字兩種。本地套接字的名字是Linux文件系統中的文件名,一般放在/tmp或/usr/tmp目錄中;網絡套接字的名字是與客戶連接的特定網絡有關的服務標識符(端口號或訪問點)。這個標識符允許Linux將進入的針對特定端口號的連接轉到正確的服務器進程。
網絡套接字不僅可用於局域網,任何帶有因特網連接(即使是一個調制解調器撥號連接)的機器都可以使用網絡套接字來彼此通信.甚至可以在一臺UNIX單機上運行基於網絡的程序,因爲UNIX計算機通常會配置一個只包含它自身的迴路(loopback)網絡.迴路網絡對調試網絡應用程序很有用,因爲它排除了任何外部網絡問題.
迴路網絡中只包含一臺計算機,傳統上它被稱爲localhost,它有一個標準的IP地址127.0.0.1(本地主機).網絡主機文件/etc/hosts中列出了本地主機地址以及在共享網絡中其他主機的名字和對應的地址.
每個與計算機進行通信的網絡都有一個與之關聯的硬件接口.一臺計算機可能在每個網絡中都有一個不同的網絡名,就會有幾個不同的IP地址.
主機字節序和網絡字節序
不同CPU中,整型值在內存空間中的保存方式是不同的.保存順序的不同意味着對接收數據的解析順序也不同.
CPU向內存保存數據的方式有2種,這意味着CPU解析數據的方式也分爲2種.
- 大端序(Big Endian):高位字節存放到低位地址
- 小端序(Little Endian):高位字節存放到高位地址
主機字節序代表CPU數據保存方式,在不同CPU中也各不相同.目前主流的Intel系列CPU以小端序方式保存數據.在通過網絡傳輸數據時約定統一方式,這種約定成爲網絡字節序—大端序.
先把數據數組轉化成大端序格式再進行網絡傳輸.所有計算機接收數據時應識別該數據是網絡字節序,小端序系統傳輸數據時應轉化爲大端序排列方式.
字節序轉換
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
htonl(host to network,long)長整數從主機字節序轉換到網絡字節序 用於IP地址轉換
htons(host to network,short)短整型從主機字節序轉換到網絡字節序 用於端口號轉換
如果計算機本身的主機字節序與網絡字節序相同,這些函數執行空操作.
數據在傳輸之前都要經過轉換嗎?
沒必要,這個過程是自動的.除了向sockaddr_in結構體變量填充數據外,其他情況無需靠需字節序問題.
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
unsigned short host_port=0x1234;
unsigned short net_port;
unsigned long host_addr=0x12345678;
unsigned long net_addr;
net_port=htons(host_port);
net_addr=htonl(host_addr);
printf("Host ordered port: %#x \n",host_port);
printf("network ordered port: %#x \n",net_port);
printf("Host ordered address: %#lx \n",host_addr);
printf("Network ordered address: %#lx \n",net_addr);
return 0;
}
網絡地址的初始化與分配
1. 將字符串信息轉換爲網絡字節序的整數型
sockaddr_in中保存地址信息的成員爲32位整數型.inet_addr函數將字符串形式的IP地址轉換成32位整數型數據.此函數在轉換類型的同時進行網絡字節序轉換.
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
成功時返回32位大端序整數型值,失敗時返回INADDR_NONE
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
char *addr1="1.2.3.4";
char *addr2="1.2.3.256";
unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n",conv_addr);
conv_addr=inet_addr(addr2);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n",conv_addr);
return 0;
}
inet_aton函數與inet_addr函數在功能上完全相同,也將字符串形式IP地址轉換爲32爲網絡字節序整數並返回.但使用了in_addr結構體.
#include <arpa/inet.h>
int inet_aton(const char * string,struct in_addr *addr);
成功返回1,失敗返回0
- string 含需轉換的IP地址信息的字符串地址值
- addr 將保存轉換結果的in_addr結構體變量的地址值
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handling(char *message);
int main(int argc,char *argv[])
{
char *addr="127.232.124.79";
struct sockaddr_in addr_inet;
if(!inet_aton(addr,&addr_inet.sin_addr))
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n",addr_inet.sin_addr.s_addr);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
exit(1);
}
2. 網絡地址初始化
struct sockaddr_in addr;
char * serv_ip=""; //聲明IP地址字符串
char * serv_port=""; //聲明端口號字符串
memset(&addr,0,sizeof(addr)); //結構體變量addr的所有成員初始化爲addr.sin_family=AF_INET; //指定地址族
addr.sin_addr.s_addr=inet_addr(serv_ip); //基於字符串的IP地址初始化
addr.sin_port=htons(atoi(serv_port)); //基於字符串的端口號初始化
atoi函數把字符串類型的值轉換成整數型.
3. 客戶端地址信息初始化
服務器端的準備工作通過bind函數完成,而客戶端則通過connect函數完成.服務器段聲明sockaddr_in結構體變量,將其初始化爲賦予服務器IP和套接字的端口號,然後調用bind函數;而客戶端則聲明sockaddr_in結構體,並初始化爲要與之連接的服務器端套接字的IP和端口號,然後調用connect函數.
4. INADDR_ANY
INADDR_ANY等價於inet_addr(“0.0.0.0”);
當服務器的監聽地址是INADDR_ANY時,會監聽服務器上所有的網卡.若採用這種方式,則可自動獲取運行服務器端的計算機IP地址.若同一計算機中一分配多個IP地址,則只要端口號一致,就可以從不同的IP地址接收數據.
實例1:
//server
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// AF_INET 表示採用TCP/IP協議族
// SOCK_STREAM 表示採用TCP協議
// 0是通常的默認情況
unsigned int sockSrv = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSrv;
addrSrv.sin_family = AF_INET; // TCP/IP協議族
addrSrv.sin_addr.s_addr = INADDR_ANY; //inet_addr("0.0.0.0");
addrSrv.sin_port = htons(8888); // socket對應的端口
// 將socket綁定到某個IP和端口(IP標識主機,端口標識通信進程)
bind(sockSrv,(struct sockaddr*)&addrSrv, sizeof(addrSrv));
// 將socket設置爲監聽模式,5表示等待連接隊列的最大長度
listen(sockSrv, 5);
struct sockaddr_in addrClient;
int len = sizeof(addrClient);
while(1)
{
// sockSrv爲監聽狀態下的socket
// &addrClient是緩衝區地址,保存了客戶端的IP和端口等信息
// len是包含地址信息的長度
// 如果客戶端沒有啓動,那麼程序一直停留在該函數處
unsigned int sockConn = accept(sockSrv,(struct sockaddr*)&addrClient, &len);
char sendBuf[100] = {0};
sprintf(sendBuf,"%s", inet_ntoa(addrClient.sin_addr)); // 將客戶端的IP地址保存下來
write(sockConn, sendBuf, 100); // 發送數據到客戶端
char recvBuf[100] = {0};
read(sockConn, recvBuf, 100); // 接收客戶端數據
printf("%s\n", recvBuf);
close(sockConn);
}
close(sockSrv);
return 0;
}
/*
sprintf 字符串格式化命令,主要功能是把格式化的數據寫入某個字符串中。sprintf 是個變參函數。
原型
int sprintf( char *buffer, const char *format, [ argument] … );
參數列表
buffer:char型指針,指向將要寫入的字符串的緩衝區。
format:格式化字符串。
[argument]...:可選參數,可以是任何類型的數據。
*/
//client
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
unsigned int sockClient = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrSrv;
addrSrv.sin_addr.s_addr = inet_addr("10.251.234.120");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(8888);
int ret = connect(sockClient, (struct sockaddr*)&addrSrv, sizeof(addrSrv));
char recvBuf[100] = {0};
read(sockClient, recvBuf, 100);
printf("%s\n", recvBuf);
write(sockClient, "hello world", 11);
close(sockClient);
return 0;
}
實例2:
//server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[]="Hello world!";
if(argc!=2)
{
printf("Usage : %s <port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); //服務器端實現過程中先要創建套接字.但此時的頭啊戒子尚非真正的服務器端套接字
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET;
serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_addr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr))==-1)
error_handling("bind() error");
if(listen(serv_sock,5)==-1) //調用listen函數進入等待連接請求狀態,連接請求等待隊列的長度設置爲5.此時的套接字纔是服務器端套接字.
error_handling("listen() error");
clnt_addr_size=sizeof(clnt_addr);
clnt_sock=accept(serv_sock,(struct sockaddr*) &clnt_addr,&clnt_addr_size); //調用accept函數從對頭取1個連接請求與客戶端建立連接,並返回創建的套接字 文 //件描述符.調用accept函數時若等待隊列爲空,則accept函數不會返回,直到隊列 //中出現新的客戶端連接
if(clnt_sock==-1)
error_handling("accept() error");
write(clnt_sock,message,sizeof(message)); //調用write函數向客戶端傳輸數據
close(clnt_sock); //close函數關閉連接
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
//client
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char *message);
int main(int argc,char * argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc!=3)
{
printf("Usage %s <IP> <port> \n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0); //創建準備連接服務器端的套接字,此時創建的是TCP套接字
if(sock==-1)
error_handling("socket() error");
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family=AF_INET; //結構體變量serv_addr中初始化IP和端口信息,初始化值爲目標服務器端套接字的IP和端口信息
serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr))==-1) //調用connect函數向服務器端發送連接請求
error_handling("connect() error");
str_len=read(sock,message,sizeof(message)-1); //完成連接後,接收服務器端傳輸的數據
if(str_len==-1)
error_handling("read() error");
printf("Message from server : %s \n",message);
close(sock); //接收數據後調用close函數關閉套接字,結束與服務器端的連接
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}