推薦先學習UDP協議在學習TCP協議
在UDP協議博客中講解得更詳細,看懂UDP協議就很容易理解TCP了↓↓↓
網路 UDP協議(C++|代碼通過udp協議實作客戶端與服務端之間的通信)
TCP通信編程
tcp是面向連接、可靠傳輸、面向位元組流的傳輸層協議
面向連接:必須建立了連接且保證雙方都具有資料收發的能力,才能開始通信,(udp是無連接的,只要知道對端地址就可以直接發送訊息)
可靠傳輸:傳送的資料,無差錯、不丟失、不重復、并且按序到達
面向位元組流:
通信方面也是分為客戶端和服務端

各端的操作流程:
服務端操作流程:
- 創建套接字埠:在內核中創建socket結構體,關聯行程與網卡之間的聯系
- 為套接字系結地址資訊:網路通信中的資料都必須帶有源端IP、源端埠、對端IP、對端埠、協議,這5個資訊稱為五元組,在內核創建的socket結構體中描述IP地址埠以及協議,(必須主動系結,告訴客戶端自己的地址資訊,如果不系結客戶端就不知道該發往哪個服務端了)為了告訴作業系統發往哪個IP地址,哪個埠的資料是交給我來處理的
- 開始監聽:設定套接字的一個監聽狀態,只有處于監聽狀態的套接字才會接收客戶端的連接請求,服務端會為每一個客戶端的連接請求都創建一個新的socket結構體,通過這個新建的socket結構體與客戶端進行通信
- 獲取一個新建立連接的socket的描述符:獲取到socket的操作句柄,通過這個指定的socket的操作句柄與指定的客戶端進行通信
- 收發資料:每個新建的套接字包含了完整的五元組,知道自己與誰通信,因此收發資料的時候就不用設定地址資訊了,
- 關閉套接字:釋放資源

客戶端操作流程:
- 創建套接字:在內核中創建socket結構體,關聯行程與網卡之間的聯系
- 為套接字系結地址資訊:描述在內核中創建的socket結構體的源端地址資訊;發送的資料中源端地址資訊就是系結的地址資訊(不推薦主動系結地址,降低埠沖突的概率,從而確保資料發送的安全性)
- 向服務端發起連接請求:當服務端處于監聽狀態時就可以進行連接;但是當服務端不處于監聽狀態,請求會丟失
- 收發資料:被服務端特定套接字服務
- 關閉套接字:釋放資源
舉一個足療店的例子來幫助你理解服務端與客戶端之間的通信

服務端介面資訊
1、創建套接字int socket(int domain, int type, int protocol) 引數內容(domian:地址域(本地通信-AF_LOCAL、IPv4-AF_INET、IPv6-AF_INET6等)確定本次socket通信使用哪種協議版本的地址結構,不同的協議版本有不同的地址結構;type:套接字型別(流式套接字-SOCK_STREAM、資料報套接字-SOCK_DGRAM等);protocol:協議型別(TCP-IPPROTO_TCP、UDP-IPPROTO_UDP) ,默認為0-流式默認TCP,資料報默認UDP)
回傳值:檔案描述符-非負整數, 套接字所有其他介面的操作句柄,失敗回傳-1
2、為套接字系結地址資訊int bind(int sockfd, struct sockaddr *addr, socklen_t len)引數內容(sockfd:創建套接字回傳的操作句柄;addr:要系結的地址資訊;len:要系結的地址資訊長度)
3、開始監聽listen(int sockfd, int backlog) 引數內容(sockfd:將sockfd的套接字設定為監聽狀態,并且監聽狀態后可以開始接收客戶端連接請求;backlog:同一時間的并發連接數,決定同一時間最多接收多少個客戶端的連接請求<內核中可創建套接字數量是有限的,防止存在惡意請求導致資源耗盡>)
4、獲取新建連接,從已完成連接佇列中取出一個socket,并且回傳這個socket的描述符操作句柄int accept(int sockfd, struct sockaddr* cli_addr, socklen_t *len)引數內容(sockfd:表示獲取哪個tcp服務端套接字的新建連接;cli_addr:這個新建的套接字對應的客戶端地址資訊;len:地址資訊長度) 回傳值:新建的socket套接字的描述符,也就是外部行程中對該套接字的操作句柄
5、收發資料,在tcp套接字中已經標示了五元組,因此接收資料時不需要獲取對方地址資訊,發送資料時也不需要指定對方的地址資訊,接收:ssize_t recv(int sockfd, char *buf, int len, int flag) 回傳值:成功回傳發送資料的長度,等于0表示斷開連接,小于0表示出錯,發送:ssize_t send(int sockfd, char *data, int len, int flag) 默認阻塞,緩沖區資料滿了則等待,若斷開連接觸發例外信號SIGPIPE
6、關閉套接字int close(fd)
客戶端介面資訊:將服務端的2、3、4步去掉,合成一步
1、創建套接字int socket(int domain, int type, int protocol)
2、向服務端發起連接請求int connect(int sockfd, struct sockaddr *srv_addr, int len) 引數內容(sockfd:哪個服務端發送請求連接;src_addr:服務端地址資訊,給該服務端發送請求)這個connect介面也會在sockfd客戶端的套接字socket中描述對端的地址資訊
3、收發資料,接收:ssize_t recv(int sockfd, char *buf, int len, int flag) 發送:ssize_t send(int sockfd, char *data, int len, int flag)
4、關閉套接字int close(fd)
代碼實作
創建一個類用于封裝各端的操作
tcpsocket.hpp
//tcpsocket.hpp
#include <cstdio>
#include <unistd.h>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
//該值表示用一時間能夠接收多少客戶端連接
//并非指整個通信最多接收多少客戶端連接
#define MAX_LISTEN 5
#define CHECK_RET(q) if((q) == false){return -1;}
class TcpSocket
{
public:
TcpSocket()
:_sockfd(-1)
{}
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
bool Bind(const string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
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 = MAX_LISTEN)
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
perror("listen error");
return false;
}
return true;
}
bool Accept(TcpSocket *new_sock, string *ip = NULL, uint16_t *port = NULL)
{
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
int new_fd = accept(_sockfd, (struct sockaddr*)&addr, &len);
cout << "dsdsdds";
if (new_fd < 0)
{
perror("accept error");
return false;
}
new_sock->_sockfd = new_fd;
if (ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(string *buf)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0)
{
perror("recv error");
return false;
}
else if (ret == 0)//默認阻塞,沒有資料就會等待,回傳0表示連接斷開
{
printf("connection broken\n");
return false;
}
buf->assign(tmp, ret);
return true;
}
bool Send(const string &data)
{
int ret = send(_sockfd, data.c_str(), data.size(), 0);
if (ret < 0)
{
perror("send error");
return false;
}
return true;
}
bool Close()
{
if (_sockfd > 0)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
bool Connect(const string &ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
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;
}
private:
int _sockfd;
};
tcp_srv.cpp
//tcp_srv.cpp
#include <cstdio>
#include <iostream>
#include "tcpsocket.hpp"
using namespace std;
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage:./tcp_srv ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TcpSocket lst_sock;
//穿件套接字
CHECK_RET(lst_sock.Socket());
//為套接字系結地址資訊
CHECK_RET(lst_sock.Bind(ip, port));
//開始監聽
CHECK_RET(lst_sock.Listen());
while (1)
{
TcpSocket new_sock;
//獲取連接
bool ret = lst_sock.Accept(&new_sock);
if (ret == false)
{
continue;//服務端不能因為獲取一個新建套接字失敗就退出
}
string buf;
new_sock.Recv(&buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: ";
cin >> buf;
new_sock.Send(buf);
}
lst_sock.Close();
return 0;
}
tcp_cli.cpp
//tcp_cli.cpp
#include <iostream>
#include <string>
#include "tcpsocket.hpp"
using namespace std;
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage: ./tcp_cli ip port" << endl;
return -1;
}
string srv_ip = argv[1];
uint16_t srv_port = stoi(argv[2]);
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(srv_ip, srv_port));
while (1)
{
string buf;
cout << "client say: ";
cin >> buf;
sock.Send(buf);
buf.clear();
sock.Recv(&buf);
cout << "server say: "<< buf << endl;
}
sock.Close();
return 0;
}
makefile
all:tcp_srv tcp_cli
tcp_srv:tcp_srv.cpp
g++ -std=c++11 $^ -o $@
tcp_cli:tcp_cli.cpp
g++ -std=c++11 $^ -o $@
查看網卡資訊

先運行服務端,等待新的連接,

運行客戶端,并發送訊息

服務端收到訊息并回復

客戶端收到訊息并回復

但是此時客戶端并沒有接收到客戶端發來的訊息,一直停留在上一次發送訊息完后的樣子

簡單分析服務端while回圈,while回圈中第一步獲取連接時阻塞等待的,當一個新的客戶端來是會為它創建一個新的套接字與它通信,客戶端第一次發送訊息,服務端走到第二步接收訊息,再走到第三步發送訊息,發送完訊息第一次回圈就結束了,又重新到了第一步等待獲取連接,此時新創建的套接字丟失,所以此時客戶端再給服務端發送訊息,就無法接收,

這時候我們就得引入多執行緒或者多行程來完成這項任務,在獲取到一個新的連接時,就啟動一個新的執行流,讓這個新的執行流去與該客戶端進行通信,這樣子做的好處是:沒有了因為新連接到來的阻塞,就不會影響與客戶端之間的通信;與客戶端通信時的阻塞,并不會影響獲取新的連接
多行程使用TCP實作通信
只要修改服務端
tcp_process.cpp
//tcp_process.cpp
#include <cstdio>
#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include "tcpsocket.hpp"
using namespace std;
void sigcb(int no)
{
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage:./tcp_srv ip port" << endl;
return -1;
}
//子行程退出處理方式
signal(SIGCHLD, sigcb);
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TcpSocket lst_sock;
//穿件套接字
CHECK_RET(lst_sock.Socket());
//為套接字系結地址資訊
CHECK_RET(lst_sock.Bind(ip, port));
//開始監聽
CHECK_RET(lst_sock.Listen());
while (1)
{
TcpSocket new_sock;
//獲取連接
bool ret = lst_sock.Accept(&new_sock);
if (ret == false)
{
continue;//服務端不能因為獲取一個新建套接字失敗就退出
}
//創建子行程
int pid = fork();
if (pid == 0)
{
while (1)
{
string buf;
new_sock.Recv(&buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: ";
cin >> buf;
new_sock.Send(buf);
}
new_sock.Close();
exit(0);
}
new_sock.Close();//父行程關閉自己不使用的socket,不影響子行程
}
lst_sock.Close();
return 0;
}
先執行服務端

再執行客戶端并發送訊息

服務端收到訊息并回復

客戶端收到并發送訊息

服務端收到訊息并回復

我們發現都已經正常完成tcp的通信了!
使用多行程實作注意事項:父子行程資料獨有,父行程用不到新建套接字,要記得關閉;行程等待使用信號處理,不再阻塞父行程
多執行緒使用TCP實作通信
多執行緒也就是穿件一個新的執行緒,讓該執行緒專門負責去與新建連接的客戶去通信,原理和多行程差不多,但是存在一個問題,執行緒如何拿到新建的套接字物件呢?new_sock是一個區域物件,回圈完畢的時候就會被釋放,傳地址就會造成錯誤,兩種方案:1、每次accept的時候new_sock在堆上申請一個新的,不會被自動釋放,但是也要防止第二次獲取的時候覆寫上次的值;2、或者直接傳值–在函式堆疊中會新建空間存盤值,僅限于資料占用空間比較小,防止堆疊溢位
我們接下來演示第二種方式將獲取連接的套接字傳到執行緒入口函式中,但是最后一個引數必須是void*型別的,我們并不能直接將物件當做值直接傳過去,我們知道物件中只有一個屬性,也就是套接字的操作句柄,我們只要將它傳過去,就能利用該操作句柄創建一個一樣的套接字,這樣子就可以拿到獲取連接時的套接字了,但是我們必須在封裝套接字的類中額外提供兩個函式,一個是獲取操作句柄的函式GetFd和設定操作句柄的函式SetFd,SetFd函式時在執行緒穿建套接字的時候使用的,在執行緒中穿建一個新的套接字,將這個套接字的操作句柄修改為傳過來的值,就能操作原先的套接字了
tcpsocket.hpp (只提供修改部分)
//tcpsocket.hpp
int GetFd()
{
return _sockfd;
}
void SetFd(int fd)
{
_sockfd = fd;
}
tcp_thread.cpp
#include <cstdio>
#include <iostream>
#include <pthread.h>
#include "tcpsocket.hpp"
using namespace std;
//執行緒入口函式,賦值收發資料通信
void* thr_start(void *arg)
{
long fd = (long)arg;
TcpSocket new_sock;
new_sock.SetFd(fd);
while (1)
{
string buf;
new_sock.Recv(&buf);
cout << "client say: " << buf << endl;
buf.clear();
cout << "server say: ";
cin >> buf;
new_sock.Send(buf);
}
new_sock.Close();//執行緒之間檔案描述符表共享,這邊關閉了描述符其他地方就用不了了
return NULL;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
cout << "Usage:./tcp_srv ip port" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = stoi(argv[2]);
TcpSocket lst_sock;
//穿件套接字
CHECK_RET(lst_sock.Socket());
//為套接字系結地址資訊
CHECK_RET(lst_sock.Bind(ip, port));
//開始監聽
CHECK_RET(lst_sock.Listen());
while (1)
{
TcpSocket new_sock;
//獲取連接
bool ret = lst_sock.Accept(&new_sock);
if (ret == false)
{
continue;//服務端不能因為獲取一個新建套接字失敗就退出
}
//執行緒
pthread_t tid;
int res = pthread_create(&tid, NULL, thr_start, (void*)new_sock.GetFd());
if (res != 0)
{
cout << "pthread create error" << endl;
continue;
}
pthread_detach(tid);//不關心執行緒回傳值,也不想等待釋放資源,因此將執行緒分離出去
}
lst_sock.Close();
return 0;
}
makefile
all:tcp_thread tcp_cli
tcp_thread:tcp_thread.cpp
g++ -std=c++11 $^ -o $@ -lpthread
udp_cli:udp_cli.cpp
g++ -std=c++11 $^ -o $@
先運行服務端

再運行客戶端并發送訊息

服務端收到訊息后回復

客戶端收到并發送訊息

服務端收到并回復

多執行緒實作注意事項:主執行緒創建執行緒之后,千萬不能關閉新建套接字,因為執行緒之間共享檔案描述符表,
服務端既可以通過多行程來實作,也可以通過多執行緒來實作,那哪種方式更合適呢?
不同場景應用不同方式解決:多執行緒靈活,資源占用少,通信方便,但是健壯性沒多行程強;多行程資源占用多,但是安全,健壯性高,
若服務端針對客戶端的業務比較簡單,使用多執行緒,效率高且通信方便;若服務端針對客戶端的業務比較復雜,使用多行程,安全性和健壯性高,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/277384.html
標籤:其他
下一篇:C++ STL中的容器配接器
