tcp編程:面向連接,可靠傳輸,面向字節流
tcp客戶端與服務端流程
-
客戶端:創建套接字,描述地址信息,發起連接請求,連接建立成功,收發數據,關閉
-
服務端:創建套接字,描述地址信息,開始監聽,接受連接請求,新建套接字,獲取新建套接字描述符,通過這個描述符與客戶端通信,關閉
socket接口介紹
- 創建套接字
int socket(int domain, int type, int protocol) - ( AF_INET, SOCK_STREAM - 流式套接字, IPPROTO_TCP);
- 綁定地址信息
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()
};
- 服務端開始監聽
int listen(int sockfd, int backlog); - 告訴操作系統開始接收連接請求
參數
- sockfd: 監聽套接字 - 獲取客戶端連接請求的套接字
- backlog: 決定同一時間,服務端所能接受的客戶端連接請求數量
SYN泛洪攻擊:
- 惡意主機不斷的向服務端主機發送大量的連接請求,若服務端爲每一個連接請求建立socket,則會瞬間資源耗盡。
服務器崩潰因此服務器端有一個connection pending queue;
- 存放爲連接請求新建的socket節點
- backlog參數決定了這個隊列的最大節點數量
- 若這個隊列放滿了,若還有新連接請求到來,則將這個後續請求丟棄掉
- 獲取新建socket的操作句柄
從內核指定socket的pending queue中取出一個socket,返回操作句柄
int accept(int sockfd, struct sockaddr *addr, socklen_t *len)
參數:
- sockfd: 監聽套接字 — 指定要獲取哪個pending queue中的套接字
- addr: 獲取一個套接字,這個套接字與指定的客戶端進行通信,通過addr獲取這個客戶端的地址信息
- len: 輸入輸出型參數 — 指定地址信息想要的長度以及返回實際的地址長度
返回值: 成功則返回新獲取的套接字的描述符; 失敗返回-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;若連接斷開觸發異常
- 關閉套接字:釋放資源
int close(int fd);
- 客戶端向服務端發送連接請求
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;
}
連接斷開在發送端與接受端上的表現:
-
接受端:連接斷開,則recv返回0(套接字寫端被關閉 — 雙工通信)
-
發送端:連接斷開,則send觸發異常 — SIGPIPE,導致進程退出