1. 程式人生 > >網路程式設計套接字(Socket)

網路程式設計套接字(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、IPv6UNIX 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_INETAF_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_ptoninet_ntop不僅可以轉換IPv4in_addr,還可以轉換IPv6in6_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的作用是將引數sockfdmyaddr繫結在一起,使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連線伺服器。connectbind的引數形式一致, 區別在於bind的引數是自己的地址, 而connect的引數是對方的地址
  • connect成功返回0,出錯返回-1