網路程式設計套接字(Socket)
網路預備
知識學習:https://blog.csdn.net/hansionz/article/details/85224786
網路程式設計套接字
一.IP地址和埠號
1.IP地址
IPv4
版本的IP地址為4位元組
,也就是32位
- 網路層的資料報中封裝
兩個IP地址
,一個源IP地址(資料報源主機的IP)
,一個目的IP地址(資料報目的主機的IP)
源IP地址
和目的IP地址
相當於取經
的例子,在預備知識
中可以看到- 一個數據報的頭部不應該只存在源IP地址和目的IP地址,還應該存在一個
協議欄位
告訴應該交給上層的哪一個協議
2.埠號
2.1 什麼是埠號
- 埠號是
傳輸層
的概念 - 埠號是一個
2位元組16位
- 埠號用來
標識一個程序
,用來告訴OS
,將資料交給哪一個程序
IP地址+埠號
可以唯一的表示網路中一個主機的程序
一個
埠號只能被一個程序佔用
2.2 埠號和程序ID
一個程序PID
也可以唯一的標識一個程序
,那為什麼還需要使用埠號
來標識一個主機中的程序呢?
- 當一個
程序退出
時,在次啟動程序
時,它的PID
已經變化。所以要將程序和埠號繫結
來標識這個程序 一個程序
可以繫結多個埠號
,但是一個埠號
只能被一個程序
繫結
2.3 源埠號和目的埠號
傳輸層
協議TCP/UDP
的資料段
中,也存在兩個欄位,一個是源埠號
,一個是目的埠號
,它用來表示這個資料是哪一個程序發的
發給哪一個程序
,也就是誰發的要發給誰
。
二.初識TCP/UDP協議和網路位元組序
1.TCP(傳輸控制協議)
傳輸層
協議- 面向
連線
,兩個主機只有建立連線
之後才可以通訊
可靠
傳輸,建立連線之後,佔用端到端的通訊資源
- 面向
位元組流
,傳送的位元組流資料之間沒有明確的間隔
2.UDP(使用者資料報協議)
傳輸層
協議- 面向
無連線
,盡最大努力交付
(例如:發簡訊,不管能不能收到,都可以傳送) 不可靠
的傳輸- 面向
資料報
,傳送的一個數據是一個整體
,它們之間有明確的間隔
3.網路位元組序
記憶體中大於1個位元組的資料相對於記憶體地址存在大小端之分,磁碟檔案中多位元組資料相對於偏移地址也存在大小端之分,網路中的資料流也存在大小端之分。
什麼是大小端之分?
單獨總結我的另一篇部落格:https://blog.csdn.net/hansionz/article/details/80871921
網路位元組流的大小端:
傳送主機
通常將傳送緩衝區
中的資料按記憶體地址從低到高
的順序發出。接收主機把從網路上接到的位元組依次儲存在接收緩衝區
中,也是按記憶體地址從低到高
的順序儲存。- 網路資料流的地址規定,
先發出
的資料是低地址
,後發出
的資料是高地址
。 TCP/IP
協議規定,網路資料流應採用大端位元組序
,即低地址高位元組
。- 不管這臺主機是
大端機
還是小端機
, 都會按照這個TCP/IP
規定的網路位元組序來傳送/接收
資料。如果當前傳送主機是小端
, 就需要先將資料轉成大端
, 否則就忽略, 直接傳送。
為使網路程式
具有可移植性
,使同樣的C程式碼在大端和小端
計算機上編譯後都能正常執行,可以呼叫以下庫函式對網路位元組序和主機位元組序
的轉換:
#include <arpa/inet.h>
//h--host主機,n--net網路,l--long長整型32位,s--short短整型16位
uint32_t htonl(uint32_t hostlong);//主機-->網路(32位)
uint16_t htons(uint16_t hostshort);//主機-->網路(16位)
uint32_t ntohl(uint32_t netlong);//網路-->主機(32位)
uint16_t ntohs(uint16_t netshort);//網路-->主機(16位)
三.Socket程式設計
1.Udp socket常見介面
- socket(建立套接字)
#include <sys/types.h>
#include <sys/socket.h>
//建立socket檔案描述符,相當於建立一個裝置檔案,來實現傳輸層之間的通訊
//(客戶端+伺服器)
int socket(int domain, int type, int protocol);
引數:
domain:代表通訊協議族
AF_INET---IPv4的協議
AF_INET6--IPv6的協議
type:代表建立什麼型別的套接字
SOCK_STREAM:流式套接字(TCP)
SOCK_DGRAM:資料報套接字(UDP)
protocol:具體的協議
0代表預設協議
返回值:
返回該套接字的檔案描述符(控制代碼)
- bind(繫結埠號)
#include <sys/types.h>
#include <sys/socket.h>
//用來繫結埠號(伺服器)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
引數:
sockfd:socket介面的返回值,既建立套接字的描述符
addr:型別為struct sockaddr,該結構體下邊有詳細說明,引數代表地址
addrlen:地址資訊長度
sockaddr結構體:
socket API
是一層抽象的網路程式設計介面,適用於各種底層網路協議,如IPv4、IPv6
、UNIX Domain Socket
。 但是各種網路協議的地址格式
並不相同:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14]; /*地址資訊*/
}
sockaddr_in結構體:
雖然socket API
的型別是sockaddr
, 但是我們真正在基於IPv4
程式設計時, 使用的資料結構是sockaddr_in
。 這個結構裡主要有三部分資訊地址型別、埠號、IP地址
。
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
//in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
可以將sockaddr
類似於void*
,將sockaddr_in
類似於int*
:
- IPv4和IPv6的地址格式定義在
netinet/in.h
中 - IPv4地址用
sockaddr_in
結構體表示,包括16位地址
型別,16 位埠號
和32位IP地址
IPv4、IPv6
地址型別分別定義為常數AF_INET
、AF_INET6
。只要取得某種sockaddr
結構體的首地址,不需要知道具體是哪種型別的sockaddr
結構體,就可以根據地址型別欄位
確定結構體中的內容socket API
可以都用struct sockaddr *
型別表示,在使用的時候需要強制轉化成sockaddr_in
。這樣的好處是程式的通用性,可以接收IPv4、IPv6、UNIX Domain Socket
各種型別的sockaddr
結構體指標做為引數
2.地址轉化函式
在sockaddr_in
中的成員struct in_addr sin_addr
表示32位
的IP
地址。但是實際中,我們通常使用點分十進位制的字串
來標識IP地址。所以,我們應該使用一些地址轉化函式來對IP地址進行字串到in_addr
的相互
轉化。
- 字串轉
in_addr
的函式
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
int inet_pton(int af, const char *src, void *dst);
in_addr
轉字串的函式
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
注:其中inet_pton
和inet_ntop
不僅可以轉換IPv4
的in_addr
,還可以轉換IPv6
的in6_addr
。
inet_ntoa函式:
inet_ntoa
函式返回了一個char*
, 這個函式自己在內部為我們申請了一塊記憶體
來儲存ip的結果。man手冊上說,inet_ntoa
函式將返回結果放到了靜態儲存區
, 不需要我們手動釋放
。但是如果放在靜態區
的話,這個函式多次被呼叫就會覆蓋掉靜態區
的內容。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
using namespace std;
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0x0;
addr2.sin_addr.s_addr = 0xffffffff;
char* p1 = inet_ntoa(addr1.sin_addr);
char* p2 = inet_ntoa(addr2.sin_addr);
cout << p1 << endl << p2 << endl;
return 0;
}
下邊的執行結果可以看出IP地址確實被覆蓋
了。
就上述情況而言,如果是多個執行緒去呼叫inet_ntoa
函式,會不會出現問題?
- Man手冊明確提出
inet_ntoa
不是執行緒安全
的函式 - 但是我自己在
centos7
上測試,並沒有出現問題, 可能內部的實現加了互斥鎖
- 多執行緒環境下,推薦使用
inet_ntop
, 這個函式由呼叫者
提供一個緩衝區
儲存結果,可以規避執行緒安全
問題
測試程式碼:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;
void* Routine1(void* arg)
{
struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
while(1)
{
sleep(1);
char* p = inet_ntoa(ptr->sin_addr);
cout << "pthread 1:" << p << endl;
}
return NULL;
}
void* Routine2(void* arg)
{
struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
while(1)
{
sleep(1);
char* p = inet_ntoa(ptr->sin_addr);
cout << "pthread 2:" << p << endl;
}
return NULL;
}
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0x0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_t t1;
pthread_t t2;
pthread_create(&t1, NULL, Routine1, (void*)&addr1);
pthread_create(&t2, NULL, Routine2, (void*)&addr2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
測試結果:
3. 常見Tcp sockect介面
- sockect(建立套接字)
引數及返回值說明:
- 該函式開啟一個
網路通訊埠
,成功像open
一樣返回一個檔案描述符
,調用出錯返回1 - 應用程式可以像讀寫檔案一樣用
read/write
在網路上收發資料 - IPv4協議,
domain
引數指定為AF_INET
TCP
協議,type引數指定為SOCK_STREAM
,表示面向位元組流
的傳輸協議protocol
引數預設為0即可
- Bind(繫結)
引數及返回值說明:
- 伺服器程式所監聽的
網路地址
和埠號
通常是固定不變的,客戶端程式得知伺服器程式的IP地址和埠號後就可以向伺服器發起連線。伺服器需要呼叫bind繫結
一個固定的網路地址和埠號。而客戶端
不需要。bind
的作用是將引數sockfd
和myaddr
繫結在一起,使sockfd
這個用於網路通訊的檔案描述符監聽addr
所描述的地址和埠號
。 struct sockaddr *
是一個通用指標型別,addr
引數實際上可以接受多種協議的sockaddr
結構體,而它們的長度各不相同,所以需要第三個引數addrlen
指定結構體的長度。bind
成功返回0,失敗返回-1。
- listen(監聽)
引數及返回值說明:
listen
宣告sockfd
處於監聽狀態
, 並且最多允許有backlog
個客戶端處於連線等待狀態
, 如果接收到更多的連線請求就忽略,這裡設定不會太大(一般是5)
。設定backlog是為了更加合理的利用資源,假設我們沒有設定backlog,所有的連線都被佔滿,當來一個連線時,我們將它拒絕,而這時,有一個連線釋放就會導致這個資源的不合理利用。但是太大也不行,太大會導致排在後邊的連線可能要等待很長的時間。listen
成功返回0,失敗返回-1。
- accept(伺服器接收連線請求)
引數及返回值說明:
-
三次握手
完成後,伺服器呼叫accept
接受連線。如果伺服器呼叫accept
時還沒有客戶端的連線請求,就阻塞等待
直到有客戶端連線上來 -
addr
是一個輸出型引數,accept
返回時傳出客戶端的地址和埠號
。如果給addr 引數傳NULL
,表示不關心
客戶端的地址 -
addrlen
引數是一個傳入傳出引數(value-result argument)
, 傳入的是呼叫者提供的,緩衝區addr
的長度以避免緩衝區溢位
問題, 傳出的是客戶端地址結構體的實際長度
(有可能沒有佔滿呼叫者提供的緩衝區)。 -
accept函式
返回值是一個新
的套接字描述符。新老套接字描述符就像古代飯店拉客的情況,老的套接字描述符是為了建立連線(對應門外拉客的工作人員),而新的套接字描述符是為了後續和客戶端通訊(對應室內的服務人員)。 -
connect(客戶端請求建立連線)
引數及返回值說明:
- 客戶端需要呼叫
connect
連線伺服器。connect
和bind
的引數形式一致, 區別在於bind
的引數是自己的地址
, 而connect
的引數是對方的地址
connect
成功返回0,出錯返回-1