Linux:socket套接字介紹及實現簡單的udp通信


socket套接字編程

socket是一套網絡編程接口,類似於中間件;上層用戶可以通過這些接口簡單的完成網絡通信傳輸;而不需要過於關心內部的實現過程。套接字編程就是通過socket接口實現網絡通信

通常情況下程序員接所接觸到的套接字(Socket)爲兩類:

  • 流式套接字(SOCK_STREAM):一種面向連接的Socket,針對於面向連接的TCP 服務應用;
  • 數據報式套接字(SOCK_DGRAM):一種無連接的Socket,對應於無連接的UDP 服務應用。

從用戶的角度來看,SOCK_STREAM、SOCK_DGRAM 這兩類套接字似乎的確涵蓋了TCP/IP 應用的全部,因爲基於TCP/IP 的應用,從協議棧的層次上講,在傳輸層的確只可能建立於TCP 或 UDP協議之上,而SOCK_STREAM、SOCK_DGRAM 又分別對應於TCP和UDP,所以幾乎所有的應用都可以用這兩類套接字實現。

在這裏插入圖片描述

socket編程:tcp/dup

傳輸層的兩個協議:tcp/udp(這兩個協議特性各有不同,因此實現流程也稍有差別)

  • tcp:傳輸控制協議:面向連接,可靠傳輸,面向字節流

應用場景: 數據安全性大於實時性的場景 - 文件傳輸

面向字節流:基於連接的,可靠的,有序的,雙向的字節流傳輸服務(以字節爲單位,不限制上層傳輸數據大小的方式)

  • udp:用戶數據報協議:無連接,不可靠,面向數據報

應用場景:

就是數據實時性大於安全性的場景 - 視頻傳輸

面向數據報:無連接的,不可靠的,無序的,有最大長度限制的數據傳輸服務

網絡通信

網絡中的兩端主機上的進程之間的通信;這兩端有個名稱:客戶端/服務器端

  • 客戶端:是主動發出請求的一方主機
  • 服務器端:是被動接受請求的一方主機

注意:

永遠都是客戶端主機先向服務器端發送請求 :

  • udp是不可靠的,不關心是否響應,不關心數據是否丟失了
  • tcp是可靠傳輸,需要先建立連接,若服務器不響應,則連接都無法建立

這意味着客戶端必須知道服務端的地址信息纔可以在發送數據的時候,將數據能夠層層封裝完成(網絡傳輸的數據都應該包含:源IP地址/目的IP地址/源端口/目的端口/協議)

  • 服務端只能綁定的是服務端主機上的IP地址;客戶端也綁定的是自己主機上的IP地址

網絡字節序和主機字節序的轉換

爲使網絡程序具有可移植性,使同樣的C代碼在大端和小端計算機上編譯後都能正常運行,可以調用以下庫函數做網絡字節序和主機字節序的轉換。

在這裏插入圖片描述

  • 這些函數名很好記,h表示host,n表示network,l表示32位長整數(四個字節),s表示16位短整數(兩個字節)。
  • 例如htonl表示將32位的長整數從主機字節序轉換爲網絡字節序,例如將IP地址轉換後準備發送。
  • 如果主機是小端字節序,這些函數將參數做相應的大小端轉換然後返回;
  • 如果主機是大端字節序,這些函數不做轉換,將參數原封不動地返回。

地址轉換函數

sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。
但是我們通常用點分十進制的字符串表示IP 地址,以下函數可以在字符串表示和in_addr表示之間轉換;

字符串轉in_addr的函數:

#include <arpa/inet.t>

uint32_t inet_addr(const char *ip); --- 將點分十進制字符串IP地址轉換爲網絡字節序整數IP地址
int inet_pton(int domain, char *src, void *dst); --- 將字符串src的IP地址按照domain地址域轉換網絡字節序的IP地址

in_addr轉字符串的函數:

char *inet_ntoa(struct in_addr addr); --- 網絡字節序整數IP地址轉換爲點分十進制字符串
int inet_ntop(int domain, void *src, char *dst, int len); --- 將網絡字節序數據按照domain地址域轉換爲字符串IP地址

其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void * addrptr。

關於inet_ntoa

inet_ntoa這個函數返回了一個char*, 很顯然是這個函數自己在內部爲我們申請了一塊內存來保存ip的結果. 那麼是否需要調用者手動釋放呢?

在這裏插入圖片描述

man手冊上說, inet_ntoa函數, 是把這個返回結果放到了靜態存儲區. 這個時候不需要我們手動進行釋放.

udp 網絡通信程序編程流程

在這裏插入圖片描述

socket 編程接口介紹

  1. 創建套接字
int socket(int domain, int type, int protocol);

參數:

  • domain: 地址域 - 不同的網絡地址結構 AF_INET - IPv4地址域
  • type: 套接字類型 - 流式套接字/數據報套接字
    • 流式套接字(SOCK_STREAM):一種有序的,可靠的,雙向的,基於連接的字節流傳輸
    • 數據報套接字(SOCK_DGRAM):無連接的,不可靠的,有最大長度限制的傳輸
  • protocol: 使用的協議( 0 - 不同套接字類型下的默認協議:流式套接字默認是tcp;數據報套接字默認是udp)
    • IPPROTO_TCP - tcp協議
    • IPPROTO_UDP - udp協議

返回值: 返回套接字的操作句柄(文件描述符)

  1. 爲套接字綁定地址信息
int bind(int sockfd, const struct sockaddr *address, socklen_t address_len);

參數:

  • sockfd: 創建套接字返回的操作句柄
  • address: 要綁定的地址信息結構
  • address_len: 地址信息的長度

返回值: 成功返回0;失敗返回-1

用戶先定義sockaddr_in的IPv4地址結構,強轉之後傳入bind之中

bind(sockaddr*){
    if (sockaddr->sin_family == AF_INET){
        這是ipv4地址結構的解析
    }else if(sockaddr->sin_family == AF_INET6){
        這是ipv6地址結構的解析
    }
}
  1. 發送數據
int sendto(int sockfd, char * data, int data_len, int flag, struct sockaddr *dest_addr, socklen_t addr_len);

參數:

  • sockfd: 套接字操作句柄。(發送數據就是將數據拷貝到內核的socket發送緩衝區中)

  • data: 要發送的數據的首地址

  • data_len: 要發送的數據的長度

  • flag: 選項參數

    • 默認爲0 - 表示當前操作是阻塞操作
    • MSG_DONTWAIT - 設置爲非阻塞

    若發送數據的時候,socket發送緩衝區已經滿了,則0默認阻塞等待;MSG_DONTWAIT就是立即報錯返回了

  • dest_addr: 目的端地址信息結構(表示數據要發送給誰)
    每一條數據都要描述源端信息(綁定的地址信息)和對端信息(當前賦予的信息)

  • addr_len: 地址信息結構長度

返回值: 成功返回實際發送的數據字節數;失敗返回-1

  1. 接收數據
int recvfrom(int sockfd, char *buf, int len, int flag, struct sockaddr *src_addr, socklen_t *addr_len);

參數:

  • sockfd: 套接字操作句柄
  • buf: 緩衝區的首地址(用於存放接收到的數據,從內核socket接收緩衝區中取出數據放入這個buf用戶態緩衝區中)
  • len:用戶想要讀取的數據長度,但是不能大於buf緩衝區的長度
  • flag:
    • 0 - 默認阻塞操作(若緩衝區中沒有數據則一直等待)
    • MSG_DONTWAIT - 非阻塞
  • src_addr:接受到的數據的源端地址 - 表示這個數據是誰發的,從哪來的(回覆的時候就是對這個地址回覆)
  • addr_len: 輸入輸出型參數,用於指定想要獲取多長的地址信息;獲取地址之後,用於返回地址信息的實際長度

返回值:成功返回實際接收到數據字節長度;失敗返回-1

  1. 關閉套接字
int close(int fd);

sockaddr結構

socket API是一層抽象的網絡編程接口,適用於各種底層網絡協議,如IPv4、IPv6,以及UNIX Domain Socket. 然而, 各種網絡協議的地址格式並不相同.

在這裏插入圖片描述

  • 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結構體指針做爲參數;

sockaddr 結構
在這裏插入圖片描述

sockaddr_in 結構

在這裏插入圖片描述

雖然socket api的接口是sockaddr, 但是我們真正在基於IPv4編程時, 使用的數據結構是sockaddr_in; 這個結構裏主要有三部分信息: 地址類型, 端口號, IP地址.

in_addr結構
在這裏插入圖片描述

in_addr用來表示一個IPv4的IP地址. 其實就是一個32位的整數;

實現一個簡單的udp客戶端/服務端

  • udp客戶端程序

C++封裝一個udpsocket類,向外提供簡單接口就能實現一個客戶端/服務端

代碼示例

// 這個demo封裝一個udpsocket類,向外提供簡單的接口實現套接字編程

#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>	// close接口
#include <stdlib.h>	// atio接口
#include <netinet/in.h>	// 地址結構定義
#include <arpa/inet.h>	// 字節序轉換接口
#include <sys/socket.h>	// 套接字接口

class UdpSocket{
public:
	UdpSocket():_sockfd(-1){
	}
	// 1.創建套接字
	bool Socket(){
		_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
		if(_sockfd < 0){
			perror("socket error");
			return false;
		}
		return true;
	}
	// 2.爲套接字綁定地址信息
	bool Bind(const std::string &ip, uint32_t port){
		// 1.定義IPv4地址結構
		struct sockaddr_in addr;
		addr.sin_family = AF_INET;
		addr.sin_port = htons(port);
		addr.sin_addr.s_addr = inet_addr(ip.c_str());
		// 2.綁定地址
		socklen_t len = sizeof(struct sockaddr_in);
		// bind(描述符, 統一地址結構sockaddr*, 地址信息長度)
		int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("bind error");
			return false;
		}
		return true;
	}
	// 3.發送數據
	bool Send(const std::string &data, const std::string &ip, uint16_t port){
		// sendto(描述符,數據,長度,選項,對端地址,地址長度)
		// 1.定義對端地址信息的IPv4地址結構
		struct sockaddr_in addr;
		addr.sin_family = AF_INET;
		addr.sin_port = htons(port);
		addr.sin_addr.s_addr = inet_addr(ip.c_str());
		// 2.向這個地址發送數據
		socklen_t len = sizeof(struct sockaddr_in);
		int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("sendto error");
			return false;
		}
		return true;
	}
	// 輸入型參數使用 const 引用;輸出型參數使用 指針;輸入輸出型使用引用
	// 4.接收數據
	bool Recv(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL){
		// recvfrom(描述符,緩衝區,數據長度,選項,對端地址,地址長度)
		struct sockaddr_in addr;	// 用於獲取發送端地址信息
		socklen_t len = sizeof(struct sockaddr_in);	// 指定地址長度以及獲取實際地址長度
		int ret;
		char tmp[4096] = {0};	// 臨時用於存放數據的緩衝區
		ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&addr, &len);
		if(ret < 0){
			perror("recvfrom error");
			return -1;
		}
		buf->assign(tmp, ret);	// 給buf申請ret大小的空間,從tmp中拷貝ret長度的數據進去
		// 爲了接口靈活,用戶若不想獲取地址信息,則不再轉換獲取
		// 只有當用戶想要獲取地址的時候,這時候傳入緩衝區,我們將數據寫入進去
		if(ip != NULL){
			*ip = inet_ntoa(addr.sin_addr);	// 將網絡字節序整數IP地址轉換爲字符串地址,返回
		}
		if(port != NULL){
			*port = ntohs(addr.sin_port);
		}
		return true;
	}
	// 5.關閉套接字
	void Close(){
		close(_sockfd);
		_sockfd = -1;
		return ;
	}
private:
	// 貫穿全文的套接字描述符
	int _sockfd;
	
};

#define CHECK_RET(q) if((q) == false){ return -1; }

// 客戶端要給服務端發送數據,那麼就需要知道服務端的地址信息
// 因此通過程序運行參數傳入服務端的地址信息
int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em: ./udp_cli 192.168.122.132 9000\n");
		return -1;
	}
	// argv[0] = ./udp_cli;
	// argv[1] = 192.168.122.132
	// argv[2] = 9000
	std::string ip_addr = argv[1];	// 服務端地址信息
	uint16_t port_addr = atoi(argv[2]);

	UdpSocket sock;
	CHECK_RET(sock.Socket());	// 創建套接字
	//CHECK_RET(sock.Bind());	// 綁定地址信息
	while(1){
		// 獲取一個標準輸入的數據,進行發送
		std::cout << "client say: ";
		fflush(stdout);
		std::string buf;
		std::cin >> buf;	// 獲取標準輸入的數據
		sock.Send(buf, ip_addr, port_addr);	// 向指定的主機進程發送buf數據
		buf.clear();	// 清空buf緩衝區
		sock.Recv(&buf);// 因爲本身客戶端就知道服務端的地址,因此不需要再獲取了
		std::cout << "server say: " << buf << std::endl;
	}
	sock.Close();

	return 0;
}
  • udp服務端程序

使用C語言編寫

代碼示例:

// 編寫一個udp服務端的C語言程序

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>	// sockaddr結構體/IPPROTO_UDP
#include <arpa/inet.h>	// 包含一些字節序轉換的接口
#include <sys/socket.h>	// 套接字接口頭文件

int main(int argc, char *argv[]){
	// argc表示參數個數,通過argv向程序傳遞端口參數
	if(argc != 3){
		printf("./udp_srv ip port  em: ./udp_srv 192.168.122.132 3000\n");
		return -1;
	}
	const char * ip_addr = argv[1];
	uint16_t port_addr = atoi(argv[2]);

	// socket(地址域,套接字類型,協議類型)
	int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	if(sockfd < 0){
		perror("socket error");
		return -1;
	}
	// bind(套接字描述符, 地址結構,地址長度)
	// struct sockaddr_in ipv4地址結構
	// struct in_addr{ uint32_t s_addr }
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	// htons - 將兩個字節的主機字節序整數轉換爲網絡字節序的整數
	addr.sin_port = htons(port_addr);	// 注意千萬不要使用htonl(端口是2個字節大小)
	// inet_addr 將一個點分十進制的字符串IP地址轉換爲網絡字節序的整數IP地址
	addr.sin_addr.s_addr = inet_addr(ip_addr);
	socklen_t len = sizeof(struct sockaddr_in);	// 獲取IPv4地址結構長度
	int ret = bind(sockfd, (struct sockaddr*)&addr, len);
	if(ret < 0){
		perror("bind error");
		return -1;
	}
	
	while(1){
		char buf[1024] = { 0 };
		struct sockaddr_in cli_addr;
		socklen_t len = sizeof(struct sockaddr_in);
		// recvfrom(描述符,緩衝區,長度,參數,客戶端地址信息,地址信息長度)
		// 阻塞接收數據,將數據放入buf中,將發送端的地址放入cliaddr中
		int ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cli_addr, &len);
		if(ret < 0){
			perror("recfrom error");
			close(sockfd);	// 關閉套接字
			return -1;
		}
		printf("client say: %s\n", buf);

		memset(buf, 0x00, 1024);	// 清空buf中的數據
		printf("server say: ");
		fflush(stdout);	// 用戶輸入數據,發送給客戶端
		scanf("%s", buf);
		// 通過sockfd將buf中的數據發送到cli_addr客戶端
		ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cli_addr, len);
		if(ret < 0){
			perror("sendto error");
			close(sockfd);	// 關閉套接字
			return -1;
		}
	}
	close(sockfd);	// 關閉套接字
}

在這裏插入圖片描述

客戶端程序中流程的特殊之處:

客戶端通常不推薦用戶自己綁定地址信息,而是讓操作系統在發送數據的時候發現socket還沒有綁定地址,然後自動選擇一個合適的ip地址和端口進行綁定。

  1. 如果不主動綁定,操作系統會選擇一個合適的地址信息進行綁定(什麼地址就是合適的地址? — 當前沒有被使用的端口
  • 一個端口只能被一個進程佔用,若用戶自己指定端口以及地址進行綁定有可能這個端口已經被使用了,則會綁定失敗

  • 讓操作系統選擇合適的端口信息,可以盡最大能力避免端口衝突的概率

  • 對於客戶端來說,不關心是用什麼源端地址將數據發送出去,只要能夠發送數據並且接受數據就可以

  1. 服務端可不可以也不主動綁定地址? — 不可以的
  • 客戶端所知道的服務端的地址信息,都是服務器告訴客戶端的
  • 一旦服務端不主動綁定地址,則會造成操作系統隨意選擇合適的地址進行綁定,服務端自己都不確定自己用了什麼地址信息,
  • 如何告訴客戶端? — 服務端通常必須主動綁定地址,並且不能隨意改動

每個程序綁定的都是自己的網卡地址信息
客戶端發送的對端地址信息一定是服務端綁定的地址信息
服務端綁定的地址信息,一定是自己主機上的網卡IP地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章