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


tcp編程:面向連接,可靠傳輸,面向字節流

tcp客戶端與服務端流程

  • 客戶端:創建套接字,描述地址信息,發起連接請求,連接建立成功,收發數據,關閉

  • 服務端:創建套接字,描述地址信息,開始監聽,接受連接請求,新建套接字,獲取新建套接字描述符,通過這個描述符與客戶端通信,關閉

在這裏插入圖片描述

socket接口介紹

  1. 創建套接字
int socket(int domain, int type, int protocol)  - ( AF_INET, SOCK_STREAM - 流式套接字, IPPROTO_TCP);

  1. 綁定地址信息
int bind(int sockfd, struct sockaddr *addr, socklen_t len); 
struct sockaddr_in{
	sin_family = AF_INET; 
	sin_port = htons(); 
	sin_addr.s_ddr = int _addr()
};
  1. 服務端開始監聽
int listen(int sockfd, int backlog); - 告訴操作系統開始接收連接請求

參數

  • sockfd: 監聽套接字 - 獲取客戶端連接請求的套接字
  • backlog: 決定同一時間,服務端所能接受的客戶端連接請求數量

SYN泛洪攻擊

  • 惡意主機不斷的向服務端主機發送大量的連接請求,若服務端爲每一個連接請求建立socket,則會瞬間資源耗盡。

服務器崩潰因此服務器端有一個connection pending queue;

  • 存放爲連接請求新建的socket節點
  • backlog參數決定了這個隊列的最大節點數量
  • 若這個隊列放滿了,若還有新連接請求到來,則將這個後續請求丟棄掉

在這裏插入圖片描述

  1. 獲取新建socket的操作句柄

從內核指定socket的pending queue中取出一個socket,返回操作句柄

int accept(int sockfd, struct sockaddr *addr, socklen_t *len)

參數:

  • sockfd: 監聽套接字 — 指定要獲取哪個pending queue中的套接字
  • addr: 獲取一個套接字,這個套接字與指定的客戶端進行通信,通過addr獲取這個客戶端的地址信息
  • len: 輸入輸出型參數 — 指定地址信息想要的長度以及返回實際的地址長度

返回值: 成功則返回新獲取的套接字的描述符; 失敗返回-1

  1. 通過新獲取的套接字操作句柄(accept返回的描述符)與指定的客戶端進行通信

接收數據:

ssize_t recv(int sockfd - accept返回的新建套接字描述符, char *buf, int len, int flag);    

返回值: 成功返回實際讀取的數據長度,連接斷開返回0;讀取失敗返回-1

發送數據:

ssize_t send(int sockfd, char *data, int len, int flag); 

返回值:成功返回實際發送的數據長度;失敗返回-1;若連接斷開觸發異常

  1. 關閉套接字:釋放資源
int close(int fd);
  1. 客戶端向服務端發送連接請求
int connect(int sockfd, int sockaddr *addr, socklen_t len);

參數:

  • sockfd: 客戶端套接字 — 若還未綁定地址,則操作系統會選擇合適的源端地址進行綁定
  • addr: 服務端地址信息 — struct sockaddr_in; 這個地址信息經過connect之後也會描述道socket中
  • len: 地址信息長度

實現tcp通信程序

tcpsocket.hpp

// 封裝實現一個tcpsocket類,向外提供簡單接口:
// 使外部通過實例化一個tcpsocket對象就能完成tcp通信程序的建立

#include <cstdio>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

class TcpSocket{
public:
	TcpSocket():_sockfd(-1){
	}
	int GetFd(){
		return _sockfd;
	}
	void SetFd(int fd){
		_sockfd = fd;
	}
	// 創建套接字
	bool Socket(){
		// socket(地址域,套接字類型,協議類型)
		_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if(_sockfd < 0){
			perror("socket error");
			return false;
		}
		return true;
	}
	
	void Addr(struct sockaddr_in *addr, const std::string &ip, uint16_t port){
		addr->sin_family = AF_INET;
		addr->sin_port = htons(port);
		inet_pton(AF_INET, ip.c_str(), &(addr->sin_addr.s_addr));
	}

	// 綁定地址信息
	bool Bind(const std:: string &ip, const uint16_t port){
		// 定義IPv4地址結構
		 struct sockaddr_in addr;
		 Addr(&addr, ip, port);
		 socklen_t len = sizeof(struct sockaddr_in);
		 int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
		 if(ret < 0){
			 perror("bind error");
			 return false;
		 }
		 return true;
	}

	// 服務端開始監聽
	bool Listen(int backlog = BACKLOG){
		// listen(描述符,同一時間的併發鏈接數)
		int ret = listen(_sockfd, backlog);
		if(ret < 0){
			perror("listen error");
			return false;
		}
		return true;
	}
	
	// 客戶端發起連接請求
	bool Connect(const std::string &ip, const uint16_t port){
		// 1.定義IPv4地址結構,賦予服務端地址信息
		struct sockaddr_in addr;
		Addr(&addr, ip, port);
		// 2.向服務端發起請求
		// 3.connect(客戶端描述符,服務端地址信息,地址長度)
		socklen_t len = sizeof(struct sockaddr_in);
		int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
		if(ret < 0){
			perror("connect error");
			return false;
		}
		return true;
	}
	
	// 服務端獲取新建連接
	bool Accept(TcpSocket *sock, std::string *ip = NULL, uint16_t *port = NULL){
		// accept(監聽套接字,對端地址信息,地址信息長度)返回新的描述符
		struct sockaddr_in addr;
		socklen_t len = sizeof(struct sockaddr_in);
		// 獲取新的套接字,以及這個套接字對應的對端地址信息 
		int clisockfd = accept(_sockfd, (struct sockaddr*)&addr, &len);
		if(clisockfd < 0){
			perror("accept error");
			return false;
		}
		// 用戶傳入了一個Tcpsocket對象的指針
		// 爲這個對象的描述符進行賦值 --- 賦值爲新建套接字的描述符
		// 後續與客戶端的通信通過這個對象就可以完成
		sock->_sockfd = clisockfd;
		if(ip != NULL){
			*ip = inet_ntoa(addr.sin_addr);	// 網絡字節序ip->字符串IP
		}
		if(port != NULL){
			*port = ntohs(addr.sin_port);
		}
		return true;
	}
	
	// 發送數據
	bool Send(const std::string &data){
		// send(描述符,數據,數據長度,選項參數)
		int ret = send(_sockfd, data.c_str(), data.size(), 0);
		if(ret < 0){
			perror("send error");
			return false;
		}
		return true;
	}
	
	// 接收數據
	bool Recv(std::string *buf){
		// recv(描述符,緩衝區,數據長度,選項參數)
		char tmp[4096] = {0};
		int ret = recv(_sockfd, tmp, 4096, 0);
		if(ret < 0){
			perror("recv error");
			return false;
		}
		else if(ret == 0){
			printf("connection break\n");
			return false;
		}
		buf->assign(tmp, ret);	// 從tmp中拷貝ret大小的數據到buf中
		return true;
	}

	// 關閉套接字
	bool Close(){
		close(_sockfd);
		_sockfd = -1;
		return true;
	}
private:
	int _sockfd;
};

tcp_srv.cpp

// 使用封裝的TcpSocket類實例化對象實現tcp服務端程序

#include <iostream>
#include "tcpsocket.hpp"

int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em:./tcp_srv 192.168.122.132 9000\n");
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);	// stoi將字符串轉換爲數字
	
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(ip, port));
	CHECK_RET(lst_sock.Listen());
	while(1){
		TcpSocket cli_sock;
		std::string cli_ip;
		uint16_t cli_port;
		// Accept類成員函數,使用的私有成員_sockfd就是lst_sock的私有成員
		// cli_sock取地址傳入,目的是爲了獲取accept接口返回的通信套接字描述符
		bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
		if(ret == false){
			// 獲取新連接失敗,可以重新繼續獲取下一個
			continue;
		}
		printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
		// 通過新獲取的通信套接字與客戶端進行通信
		std::string buf;
		if(cli_sock.Recv(&buf) == false){
			cli_sock.Close();	// 通信套接字接收數據出錯,關閉的是通信套接字
			continue;
		}
		printf("client:[%s:%d] say:%s\n", &cli_ip[0], cli_port, &buf[0]);
		std::cout << "server say:";
		fflush(stdout);
		buf.clear();
		std::cin >> buf;

		if(cli_sock.Send(buf) == false){
			cli_sock.Close();
			continue;
		}
	}
	lst_sock.Close();
	
	return 0;
}

tcp_cli.cpp

// 通過封裝的TcpSocket類實例化對象實現tcp客戶端程序

#include <iostream>
#include "tcpsocket.hpp"

int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em:./tcp_cli 192.168.122.132 9000 - 服務綁定的地址\n");
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);
	TcpSocket cli_sock;
	
	// 創建套接字
	CHECK_RET(cli_sock.Socket());
	// 綁定地址信息(不推薦)
	// 向服務端發起請求
	CHECK_RET(cli_sock.Connect(ip, port));
	// 循環收發數據
	while(1){
		printf("client say:");
		fflush(stdout);
		std::string buf;
		std::cin >> buf;

		// 因爲客戶端不存在多種套接字的文件,因此一旦當前套接字出錯直接退出就行
		// 進程退出就會釋放資源,關閉套接字
		CHECK_RET(cli_sock.Send(buf));

		buf.clear();
		CHECK_RET(cli_sock.Recv(&buf));
		printf("server say:%s\n", buf.c_str());
	}	
	cli_sock.Close();
	return 0;
}

代碼生成
在這裏插入圖片描述

tcp服務端程序無法持續與客戶端進行通信:

在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述

具體技術:

多線程/多進程

多進程

  • 父進程創建子進程,數據獨有,各自有一份cli_sock;然而子進程通過cli_sock通信,但是父進程不需要,因此父進程關閉自己的cli_sock
  • 父進程要等待子進程退出,避免產生殭屍進程;爲了父進程只負責獲取新連接,因此對於SIGCHLD信號自定義處理回調等待

服務端代碼

// 使用封裝的TcpSocket類實例化對象實現tcp服務端程序

#include <iostream>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"

void sigcb(int signo){
	// 當子進程退出的時候就會向父進程發送SIGCHLD信號,回掉這個函數
	// waitpid返回值>0表示處理了一個退出的子進程
	// waitpid<=0 表示沒有退出的子進程
	while(waitpid(-1, 0, WNOHANG) > 0);	// 一次回調循環將退出的子進程全部處理
}

int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em:./tcp_srv 192.168.122.132 9000\n");
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);	// stoi將字符串轉換爲數字
	
	signal(SIGCHLD, sigcb);
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(ip, port));
	CHECK_RET(lst_sock.Listen());
	while(1){
		TcpSocket cli_sock;
		std::string cli_ip;
		uint16_t cli_port;
		bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
		if(ret == false){
			continue;
		}
		printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
		// ------------------------------------------------------
		pid_t pid = fork();
		if(pid == 0){	// 子進程複製父進程 - 數據獨有,代碼共享
			// 讓子進程處理與客戶端通信
			while(1){
				// 通過新獲取的通信套接字與客戶端進行通信
				std::string buf;
				if(cli_sock.Recv(&buf) == false){
					cli_sock.Close();	// 通信套接字接收數據出錯,關閉的是通信套接字
					exit(0);
				}
				printf("client:[%s:%d] say:%s\n", &cli_ip[0], cli_port, &buf[0]);
				std::cout << "server say:";
				fflush(stdout);
				buf.clear();
				std::cin >> buf;

				if(cli_sock.Send(buf) == false){
					cli_sock.Close();
					exit(0);
				}
			}
			cli_sock.Close();
			exit(0);
		}
		// 父子進程數據獨有,都會具有cli_sock,但是父進程並不通信
		cli_sock.Close();	// 這個關閉對子進程沒有影響,數據各自有一份
	}
	lst_sock.Close();
	
	return 0;
}

多線程

  • 主線程獲取到新連接然後創建新線程與客戶端進行通信,但是需要將套接字描述符傳入線程執行函數中

  • 但是傳輸這個描述符的時候,不能使用局部變量的地址傳遞(局部變量的空間在循環完畢就會被釋放),可以傳描述符的值,也可以傳入new的對象

  • c++ 中對於類型強轉,將數據值當作指針傳遞有很多限制,我們想辦法去克服就可以了

  • 主線程中雖然不使用cli_sock,但是不能關閉cli_sock,因爲線程間共享資源,一個線程釋放,另一個線程也就沒法使用了

// 使用封裝的TcpSocket類實例化對象實現tcp服務端程序

#include <iostream>
#include <stdlib.h>
#include "tcpsocket.hpp"

void *thr_start(void *arg){
	long fd = (long)arg;
	TcpSocket cli_sock;
	cli_sock.SetFd(fd);
	while(1){
		// 通過新獲取的通信套接字與客戶端進行通信
		std::string buf;
		if(cli_sock.Recv(&buf) == false){
			cli_sock.Close();	// 通信套接字接收數據出錯,關閉的是通信套接字
			pthread_exit(NULL);	// exit是退出進程
		}
		printf("client say:%s\n", &buf[0]);
		std::cout << "server say:";
		fflush(stdout);
		buf.clear();
		std::cin >> buf;
		if(cli_sock.Send(buf) == false){
			cli_sock.Close();
			pthread_exit(NULL);
		}
	}
	// 循環退出則關閉套接字
	cli_sock.Close();
	return NULL;
}

int main(int argc, char *argv[]){
	if(argc != 3){
		printf("em:./tcp_srv 192.168.122.132 9000\n");
		return -1;
	}
	std::string ip = argv[1];
	uint16_t port = std::stoi(argv[2]);	// stoi將字符串轉換爲數字
	
	TcpSocket lst_sock;
	CHECK_RET(lst_sock.Socket());
	CHECK_RET(lst_sock.Bind(ip, port));
	CHECK_RET(lst_sock.Listen());
	while(1){
		TcpSocket cli_sock;
		std::string cli_ip;
		uint16_t cli_port;
		// Accept類成員函數,使用的私有成員_sockfd就是lst_sock的私有成員
		// cli_sock取地址傳入,目的是爲了獲取accept接口返回的通信套接字描述符
		bool ret = lst_sock.Accept(&cli_sock, &cli_ip, &cli_port);
		if(ret == false){
			// 獲取新連接失敗,可以重新繼續獲取下一個
			continue;
		}
		printf("new connect: [%s:%d]\n", cli_ip.c_str(), cli_port);
		// --------------------------------------
		pthread_t tid;
		// 將通信套接字當作參數傳遞給線程,讓線程與客戶端進行通信
		// cli_sock是一個局部變量 - 循環完了這個資源就會被釋放
		pthread_create(&tid, NULL, thr_start, (void*)cli_sock.GetFd());	// 線程
		pthread_detach(tid);	// 不關心線程返回值,分離線程,退出後自動釋放資源
		// 主線程不能關閉cli_sock套接字,因爲多線程是公用描述符的
	}
	lst_sock.Close();
	
	return 0;
}

連接斷開在發送端與接受端上的表現:

  1. 接受端:連接斷開,則recv返回0(套接字寫端被關閉 — 雙工通信)

  2. 發送端:連接斷開,則send觸發異常 — SIGPIPE,導致進程退出

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