簡單的TCP網絡程序:英譯漢服務器

一、服務器的初始化

下面介紹程序中用到的socket API,這些函數都在sys/socket.h中。

1.創建套接字

socket():

?參數介紹:

  • socket()打開一個網絡通訊端口,如果成功的話,就像open()一樣返回一個文件描述符;
  • 應用程序可以像讀寫文件一樣用read/write在網絡上收發數據;
  • 如果socket()調用出錯則返回-1;
  • 對于IPv4, family參數指定為AF_INET;
  • 對于TCP協議,type參數指定為SOCK_STREAM, 表示面向流的傳輸協議
  • protocol參數的介紹從略,指定為0即可。
class TcpServer
{
public:TcpServer(): _sockfd(defaultsockfd){}void Init(){_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) // 創建套接字失敗{lg(Fatal, "create socket, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, socket: %d", _sockfd);}~TcpServer(){close(_sockfd);}private:int _sockfd; // 套接字
};

2.綁定端口號和ip

bind():

?參數介紹:

  • 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后 就可以向服務器發起連接; 服務器需要調用bind綁定一個固定的網絡地址和端口號;
  • bind()成功返回0,失敗返回-1。
  • bind()的作用是將參數sockfd和myaddr綁定在一起, 使sockfd這個用于網絡通訊的文件描述符監聽 myaddr所描述的地址和端口號;
  • 前面講過,struct sockaddr *是一個通用指針類型,myaddr參數實際上可以接受多種協議的sockaddr結 構體,而它們的長度各不相同,所以需要第三個參數addrlen指定結構體的長度;

我們先來看看地址轉換函數

本節只介紹基于IPv4的socket網絡編程,sockaddr_in中的成員struct in_addr sin_addr表示32位 的IP 地址 但是我們通常用點分十進制的字符串表示IP 地址,以下函數可以在字符串表示 和in_addr表示之間轉換;

字符串轉in_addr的函數:

in_addr轉字符串的函數:

其中inet_pton和inet_ntop不僅可以轉換IPv4的in_addr,還可以轉換IPv6的in6_addr,因此函數接口是void *addrptr。inet_ntoa這個函數返回了一個char*, 很顯然是這個函數自己在內部為我們申請了一塊內存來保存ip的結果. 那么是 否需要調用者手動釋放呢?

man手冊上說, inet_ntoa函數, 是把這個返回結果放到了靜態存儲區. 這個時候不需要我們手動進行釋放. 那么問題來了, 如果我們調用多次這個函數, 會有什么樣的效果呢? 參見如下代碼:

因為inet_ntoa把結果放到自己內部的一個靜態存儲區, 這樣第二次調用時的結果會覆蓋掉上一次的結果

  • 思考: 如果有多個線程調用 inet_ntoa, 是否會出現異常情況呢?
  • 在APUE中, 明確提出inet_ntoa不是線程安全的函數;
  • 但是在centos7上測試, 并沒有出現問題, 可能內部的實現加了互斥鎖;
  • 自己寫程序驗證一下在自己的機器上inet_ntoa是否會出現多線程的問題;
  • 在多線程環境下, 推薦使用inet_ntop, 這個函數由調用者提供一個緩沖區保存結果, 可以規避線程安全問 題;

我們的程序中對myaddr參數是這樣初始化的:

  • 1. 將整個結構體清零;
  • 2. 設置地址類型為AF_INET;
  • 3. 網絡地址為0.0.0.0, 這個宏表示本地的任意IP地址,因為服務器可能有多個網卡,每個網卡也可能綁定多個IP 地址, 這樣設置可以在所有的IP地址上監聽,直到與某個客戶端建立了連接時才確定下來到底用 哪個IP 地址;
class TcpServer
{
public:TcpServer(const uint16_t& port, const string& ip = defaultip): _sockfd(defaultsockfd), _port(port), _ip(ip){}void Init(){// 1.創建套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) // 創建套接字失敗{lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, socket: %d", _sockfd);//2.綁定端口號// 使用這個結構體需要包頭文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 轉化為網絡序列local.sin_port = htons(_port);// 字符串轉化為點時分形式的ipinet_aton(_ip.c_str(), &(local.sin_addr));//此時我們僅僅只在用戶棧填好了,并沒有寫進到打開的網絡文件和套接字中,沒有設置系統中int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));exit(BindError);}}~TcpServer(){close(_sockfd);}private:int _sockfd; // 套接字uint16_t _port; // 端口號string _ip; // ip地址
};

3.設置監聽狀態

listen():

  • listen()聲明sockfd處于監聽狀態, 并且最多允許有backlog個客戶端處于連接等待狀態, 如果接收到更多 的連接請求就忽略, 這里設置不會太大(一般是5);
  • listen()成功返回0,失敗返回-1;
#pragma onec#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要設置的太大
Log lg;enum
{UsageError = 1,SocketError = 2,BindError = 3,ListenError = 4
};class TcpServer
{
public:TcpServer(const uint16_t& port, const string& ip = defaultip): _sockfd(defaultsockfd), _port(port), _ip(ip){}void Init(){// 1.創建套接字_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0) // 創建套接字失敗{lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, socket: %d", _sockfd);//2.綁定端口號// 使用這個結構體需要包頭文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 轉化為網絡序列local.sin_port = htons(_port);// 字符串轉化為點時分形式的ipinet_aton(_ip.c_str(), &(local.sin_addr));//此時我們僅僅只在用戶棧填好了,并沒有寫進到打開的網絡文件和套接字中,沒有設置系統中int n = bind(_sockfd, (const struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket success, socket: %d", _sockfd);// 3.設置監聽狀態// Tcp是面向連接的,服務器一般是比較“被動的”,服務器一直處于一種,一直在等待連接到來的狀態if (listen(_sockfd, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}   lg(Info, "bind socket success, socket: %d", _sockfd);}~TcpServer(){close(_sockfd);}private:int _sockfd; // 套接字uint16_t _port; // 端口號string _ip; // ip地址
};

4.設置服務器的端口號和ip

未來我們向讓用戶設置端口號,所以我們可以在main函數中借助命令行參數傳遞,對于ip我們就直接使用默認的ip參數"0.0.0.0"即可。

#include "TcpServer.hpp"#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// ./tcpserver 8080
int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(UsageError);}uint16_t port = std::stoi(argv[1]);unique_ptr<TcpServer> tcpSvr(new TcpServer(port));tcpSvr->Init();//tcpSvr->Start();return 0;
}

現在我們就可以來測試一下啦!

二、服務器的運行

1.建立新鏈接

accept():

  • 三次握手完成后, 服務器調用accept()接受連接;
  • 如果服務器調用accept()時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來;
  • addr是一個傳出參數,accept()返回時傳出客戶端的地址和端口號;
  • 如果給addr參數傳NULL,表示不關心客戶端的地址;
  • addrlen參數是一個傳入傳出參數(value-result argument), 傳入的是調用者提供的, 緩沖區addr的長度 以避免緩沖區溢出問題, 傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區);
  • 獲取連接成功返回接收到的套接字的文件描述符,獲取連接失敗返回-1,同時錯誤碼會被設置。

?accept函數返回的套接字是什么?

調用accept函數獲取連接時,是從監聽套接字當中獲取的。如果accept函數獲取連接成功,此時會返回接收到的套接字對應的文件描述符。

?監聽套接字與accept函數返回的套接字的作用:

  • 監聽套接字:用于獲取客戶端發來的連接請求。accept函數會不斷從監聽套接字當中獲取新連接。
  • accept函數返回的套接字:用于為本次accept獲取到的連接提供服務。監聽套接字的任務只是不斷獲取新連接,類似于餐廳門口的迎賓,而真正為這些連接提供服務的套接字是accept函數返回的套接字,類似于餐廳的服務員,而不是監聽套接字。

所以初始化TCP服務器時創建的套接字應該叫做監聽套接字。為了表明寓意,我們將代碼中套接字的名字由_sockfd改為_listensocket,這樣寫著更清楚明了。

#pragma onec#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
using namespace std;const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要設置的太大
Log lg;enum
{UsageError = 1,SocketError = 2,BindError = 3,ListenError = 4
};class TcpServer
{
public:TcpServer(const uint16_t& port, const string& ip = defaultip): _listensocket(defaultsockfd), _port(port), _ip(ip){}void Init(){// 1.創建套接字_listensocket = socket(AF_INET, SOCK_STREAM, 0);if (_listensocket < 0) // 創建套接字失敗{lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, socket: %d", _listensocket);//2.綁定端口號// 使用這個結構體需要包頭文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 轉化為網絡序列local.sin_port = htons(_port);// 字符串轉化為點時分形式的ipinet_aton(_ip.c_str(), &(local.sin_addr));//此時我們僅僅只在用戶棧填好了,并沒有寫進到打開的網絡文件和套接字中,沒有設置系統中int n = bind(_listensocket, (const struct sockaddr*)&local, sizeof(local));if(n < 0){lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket success, socket: %d", _listensocket);// 3.設置監聽狀態// Tcp是面向連接的,服務器一般是比較“被動的”,服務器一直處于一種,一直在等待連接到來的狀態if (listen(_listensocket, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}   lg(Info, "bind socket success, socket: %d", _listensocket);}void Start(){lg(Info, "tcpServer is running....");for (;;){// 1. 獲取新連接 - 知道客戶端的ip地址和端口號struct sockaddr_in client;socklen_t len = sizeof(client);// _sockfd的核心工作是: 從底層獲取客戶端的請求 - 餐廳門口的迎賓 // sockdf的核心工作是: 處理會去來的客戶請求 - 餐廳的服務員int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);if (sockfd < 0){// 從底層獲取客戶端的請求 - 餐廳門口的迎賓 - 路人不來吃飯 - 換一下批路人lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?continue; // 所以這里使用continue}lg(Info, "get a new link..., sockfd: %d", sockfd);}}~TcpServer(){close(_listensocket);}private:int _listensocket; // 套接字uint16_t _port; // 端口號string _ip; // ip地址
};

此時我們想來測試一下,但是我們的客戶端還沒有寫,咋辦呢?

telnet 127.0.0.1 8888 

是一個網絡診斷命令,它的作用是嘗試通過Telnet協議連接到本地主機(本機)的8888端口。這里:

  • telnet?是命令本身,用于進行遠程登錄和管理。
  • 127.0.0.1?是IPv4環回地址,指向本地計算機。使用這個地址,你實際上是嘗試連接到你自己的機器。
  • 8888?是端口號,許多應用程序和服務會監聽特定的端口來接收數據。8888是一個常見的測試或備用端口。

上一個知識點我們提到udp它是不能綁定我們云服務器的公網ip的,但是綁定本地環回127.0.0.1可以的,我們看看tcp可不可以。

答案是也不可以綁定我們云服務器的公網ip的。那么綁定本地環回127.0.0.1可以嘛?

所以為了能連接我們的與服務器的公網ip,我們將服務器的ip設置為0.0.0.0,這就意味著該tcp服務器可以讀取服務器任何一個ip,保證一定能連接上我們的服務器。

2.進行通信

建立通信我們首先就要獲取到給服務器發送請求的端口號和ip地址

我們來測試一下,看看此時能不能接收到客戶端的端口號和ip地址。

此時我們就能知道我們確實有連接到我們的服務器了。

?注意:我們使用的云服務默認是將端口號禁用了,我們需要在云服務器后臺將我們的端口號取消禁用,否則的話我們只能在本地通信,我們可以使用windows連接一下服務器,看看出現上面情況。

所以就需要前往服務器進行安全組的設置。

隨后我們再來測試一下哈。

同時我們這里還有一個問題,我們之前的端口號和網絡序列都要轉成網絡序列,我們都使用了相應的接口,但是為什么用戶發送來的數據,我們不需要手動調用接口轉化為網絡序列呢?在使用套接字通信的時候,會默認將用戶發送來的數據轉化成網絡序列,而端口號和網絡序列比較特殊,需要寫給操作系統的,所以需要我們自己去轉的。現在我們就可以進行通信了,由于tcp是基于數據流的,所以直接使用write和read接口即可。

void Service( int sockfd,const string& ip,const uint16_t& port)
{char buffer[4096];while(true){// 讀取用戶發送的請求ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = '\0';cout << "client says: " << buffer << endl;string echo_string = "tcpserver echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}}
}

我們來運行一下:

此時我們發現就可以通信啦!!!但是上面的客戶端不是我們寫的,我們自己來寫一個。

三、客戶端的初始化

1.創建套接字

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{cerr << "socket error" << endl;
}

2.綁定端口號和ip

由于客戶端不需要固定的端口號,因此不必調用bind(),客戶端的端口號由內核自動分配

注意:

  • 客戶端不是不允許調用bind(), 只是沒有必要調用bind()固定一個端口號. 否則如果在同一臺機器上啟動 多個客戶端, 就會出現端口號被占用導致不能正確建立連接;
  • 服務器也不是必須調用bind(), 但如果服務器不調用bind(), 內核會自動給服務器分配監聽端口, 每次啟動 服務器時端口號都不一樣, 客戶端要連接服務器就會遇到麻煩;

測試多個連接的情況

再啟動一個客戶端, 嘗試連接服務器, 發現第二個客戶端, 不能正確的和服務器進行通信. 分析原因, 是因為我們accecpt了一個請求之后, 就在一直while循環嘗試read, 沒有繼續調用到accecpt, 導致不能接 受新的請求. 我們當前的這個TCP, 只能處理一個連接, 這是不科學的.后面我們會改成多線程版本的。

3.連接服務器

connect:

  • 客戶端需要調用connect()連接服務器;
  • connect和bind的參數形式一致, 區別在于bind的參數是自己的地址, 而connect的參數是對方的地址;
  • connect()成功返回0,出錯返回-1;
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>using namespace std;
void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;}// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// tcp客戶端要不要bind?一定要顯示綁定只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送connect的時候,進行自動隨機綁定// 可是客戶端此時不知道服務器的ip和端口號// 使用命令行參數來解決struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));int n = connect(sockfd, (const struct sockaddr*)&server, sizeof(server));if(n < 0){cerr << "connect error..." << endl;exit(2);}close(sockfd);return 0;
}

4.給服務端發送信息

string message;
while (true)
{cout << "Please Enter# ";getline(cin, message);// 發送數據int n = write(sockfd, message.c_str(), message.size());// 讀取數據char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}
}

此時我們的服務器和客戶端就可以運行啦!

四、一些安全問題的處理

1.客戶端直接退出,服務器處理

客戶端直接退出,服務器會怎么樣,服務器讀取不到客戶端發來的請求,此時read返回值為0,應該關閉網絡文件描述符,剩余一種情況就是讀取失敗,我們直接打印警告日志信息。

void Service( int sockfd,const string& ip,const uint16_t& port)
{char buffer[4096];while(true){// 讀取用戶發送的請求ssize_t n = read(sockfd, buffer, sizeof(buffer));if(n > 0){buffer[n] = '\0';cout << "client says: " << buffer << endl;string echo_string = "tcpserver echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if(n == 0){// 此時需要關閉文件描述符// 我們在返回調用該函數的地方該關閉文件描述符lg(Info, "%s:%d quit, server close sockfd: %d", ip.c_str(), port, sockfd);break;}else{lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));break;}}
}

運行一下:

2.單進程服務器,多個客戶端連接

我們直接來看現象

然后我們讓客戶端分別寫一條數據

我們發現此時客戶端1能成功發送數據并接收數據,但是客戶端2不行,隨后我們來關閉客戶端1.

此時客戶端1一退出,客戶端2立馬連接,并且將剛剛的數據發送給服務器并成功并接收數據。為什么呢?因為此時我們的服務器是一個單進程,一個服務器為一個客戶端提供服務時,此時由于我們寫的是while循環,此時必須等客戶端1退出,才能關閉文件描述符(讓),然后這個服務器才會空閑,然后此時客戶端2才能建立連接,服務器才會提供服務。此時就相當于餐廳只有一個服務員,等這個服務員服務好了客戶端1,才能接待客戶端2,所以我們這里需要改一下,讓多個客戶端共同使用。

void Start()
{lg(Info, "tcpServer is running....");for (;;){// 1. 獲取新連接 - 知道客戶端的ip地址和端口號struct sockaddr_in client;socklen_t len = sizeof(client);// _sockfd的核心工作是: 從底層獲取客戶端的請求 - 餐廳門口的迎賓 // sockdf的核心工作是: 處理會去來的客戶請求 - 餐廳的服務員int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);if (sockfd < 0){// 從底層獲取客戶端的請求 - 餐廳門口的迎賓 - 路人不來吃飯 - 換一下批路人lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?continue; // 所以這里使用continue}lg(Info, "get a new link..., sockfd: %d", sockfd);// 2.根據新連接來進行通信// 獲取客戶端的ip地址和端口號uint16_t clientport = ntohs(client.sin_port);char clientip[32];// 轉化成主機序列inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));cout << "clientport: " << clientport << ", clientip: " << clientip << endl;// 單進程版本// Service(sockfd, clientip, clientport);// close(sockfd);// 多進程版本pid_t id = fork();if(id == 0){// 子進程// 子進程會繼承父進程的文件描述符close(_listensocket);if(fork() > 0) exit(0); // 此時子進程退出了Service(sockfd, clientip, clientport); // 孫子進程執行// 對于孫子進程,它的父進程已經退出了,此時孫子進程被系統領養close(sockfd);exit(0);}// 父進程// 文件描述符使用的是引用計數// 關閉父進程的文件描述符不會影響子進程close(sockfd);// 這里等待回收子進程的方式不能是阻塞等待pid_t rid = waitpid(id, nullptr, 0);}
}

此時我們再來測試一下:

此時我們無論來多少個客戶端,我們的服務器都能解決!!!除了上面一種方法,我們還可以使用信號的方式,注意使用這個需要帶上頭文件<signal.h>

signal(SIGVHLD, SIG_IGN);

如果我們此時來了一大批客戶,那么此時就會為每一個客戶創建一個進程,而我們之前學過,創建子進程的成本是非常大的,所以此時我們可以使用多線程來解決。

#pragma onec#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"
using namespace std;const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要設置的太大
Log lg;enum
{UsageError = 1,SocketError = 2,BindError = 3,ListenError = 4
};class TcpServer;class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;string clientip;uint16_t clientport;TcpServer *tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t &port, const string &ip = defaultip): _listensocket(defaultsockfd), _port(port), _ip(ip){}void Init(){// 1.創建套接字_listensocket = socket(AF_INET, SOCK_STREAM, 0);if (_listensocket < 0) // 創建套接字失敗{lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, listensocket: %d", _listensocket);// 2.綁定端口號//  使用這個結構體需要包頭文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 轉化為網絡序列local.sin_port = htons(_port);// 字符串轉化為點時分形式的ipinet_aton(_ip.c_str(), &(local.sin_addr));// local.sin_addr.s_addr = INADDR_ANY;// 此時我們僅僅只在用戶棧填好了,并沒有寫進到打開的網絡文件和套接字中,沒有設置系統中int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket success, listensocket: %d", _listensocket);// 3.設置監聽狀態// Tcp是面向連接的,服務器一般是比較“被動的”,服務器一直處于一種,一直在等待連接到來的狀態if (listen(_listensocket, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "bind socket success, listensocket: %d", _listensocket);}void Start(){lg(Info, "tcpServer is running....");for (;;){// 1. 獲取新連接 - 知道客戶端的ip地址和端口號struct sockaddr_in client;socklen_t len = sizeof(client);// _sockfd的核心工作是: 從底層獲取客戶端的請求 - 餐廳門口的迎賓// sockdf的核心工作是: 處理會去來的客戶請求 - 餐廳的服務員int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);if (sockfd < 0){// 從底層獲取客戶端的請求 - 餐廳門口的迎賓 - 路人不來吃飯 - 換一下批路人lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?continue;                                                                      // 所以這里使用continue}lg(Info, "get a new link..., sockfd: %d", sockfd);// 2.根據新連接來進行通信// 獲取客戶端的ip地址和端口號uint16_t clientport = ntohs(client.sin_port);char clientip[32];// 轉化成主機序列inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));cout << "clientport: " << clientport << ", clientip: " << clientip << endl;// 單進程版本// Service(sockfd, clientip, clientport);// close(sockfd);// 多進程版本// pid_t id = fork();// if(id == 0)//{// 子進程// 子進程會繼承父進程的文件描述符// close(_listensocket);// if(fork() > 0) exit(0); // 此時子進程退出了// Service(sockfd, clientip, clientport); // 孫子進程執行// 對于孫子進程,它的父進程已經退出了,此時孫子進程被系統領養// close(sockfd);// exit(0);//}// 父進程// 文件描述符使用的是引用計數// 關閉父進程的文件描述符不會影響子進程// close(sockfd);// 這里等待回收子進程的方式不能是阻塞等待// pid_t rid = waitpid(id, nullptr, 0);// 多線程版本ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);pthread_t tid;pthread_create(&tid, nullptr, Rountine, td);// 這里不用join,因為它是阻塞等待// pthread_join(tid, nullptr);}}static void* Rountine(void* args){pthread_detach(pthread_self()); // 設置分離狀態// 文件描述符共享,此時我們就不能關閉// 多線程只擁有tcbThreadData *td = static_cast<ThreadData *>(args);// static靜態成員方法無法使用非靜態成員方法和成員// 1.將Service放到TcpServer類外// 2.將當前對象的this指針傳入td->tsvr->Service(td->sockfd, td->clientip, td->clientport);delete td;return nullptr;}void Service(int sockfd, const string &ip, const uint16_t &port){char buffer[4096];while (true){// 讀取用戶發送的請求ssize_t n = read(sockfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = '\0';cout << "client says: " << buffer << endl;string echo_string = "tcpserver echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){// 此時需要關閉文件描述符lg(Info, "%s:%d quit, server close sockfd: %d", ip.c_str(), port, sockfd);break;}else{lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));break;}}}~TcpServer(){close(_listensocket);}private:int _listensocket; // 套接字uint16_t _port;    // 端口號string _ip;        // ip地址
};

此時我們再來運行一下:

此時我們是每次來一個客戶端,然后就創建一個線程,也有點消耗,畢竟俺們每次都要創建,我們來寫一個線程池,一開始我們就創建一大批進程,來一個客戶端直接給它一個線程。并且我們上面的代碼還存在一個問題,我們的服務是一直服務用戶請求的,只要客戶端不退,即使客戶端不發信息,我們為該用戶創建的線程也不會銷毀,這樣就會導致系統中的線程越來越多,直接來寫代碼。

#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 10;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}void Start(){int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

我們上面實現的線程池是一個單例模式,只允許創建一個對象,并且我們的線程池可以發送任務,它可以處理好,所以我們可以把服務器接收到的任務給我們的線程池,所以我們再來寫一個任務。

#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include <string.h>extern Log lg;
using namespace std;class Task
{
public:Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}void run(){char buffer[4096];// 讀取用戶發送的請求ssize_t n = read(sockfd_, buffer, sizeof(buffer));if (n > 0){buffer[n] = '\0';cout << "client says: " << buffer << endl;string echo_string = "tcpserver echo# ";echo_string += buffer;write(sockfd_, echo_string.c_str(), echo_string.size());}else if (n == 0){// 此時需要關閉文件描述符lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else{lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));}close(sockfd_);}void operator()(){run();}~Task(){}private:int sockfd_;std::string clientip_;uint16_t clientport_;
};

此時對于客戶端,我們讓它輸入一次信息就不要再輸入了,你要是再輸入就需要重新連接服務器。

#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>using namespace std;
void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;}// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// tcp客戶端要不要bind?一定要顯示綁定只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送connect的時候,進行自動隨機綁定// 可是客戶端此時不知道服務器的ip和端口號// 使用命令行參數來解決struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server));if (n < 0){cerr << "connect error..." << endl;exit(2);}string message;cout << "Please Enter# ";getline(cin, message);// 發送數據n = write(sockfd, message.c_str(), message.size());// 讀取數據char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}close(sockfd);return 0;
}

此時我們就不需要再創建線程了,并且用戶發一次信息,服務處理完該線程就退了,用戶如果還有就需要重新連接服務器。

3.服務端寫入失敗,寫入需要判斷

此時我們再來看看結果:

此時就符合我們的預期結果啦!

4.服務器未寫,sockfd鏈接斷開

當我們的服務器已經讀到數據的時候,但是此時文件描述符的鏈接一鍵斷開,此時再向這個失效的文件描述符寫那么程序就會出問題,這個就相當于之前的管道,如果讀端關掉,那么寫端繼續寫當前進行就會收到SIGPIPE信號,將進程終止掉,此時程序就會出現SIGPIPE信號,我們直接將其直接終止。

5.客戶端多次鏈接,服務器提供多次服務

然后我們來測試一下:

這里我們再次鏈接的到時候為什么失敗了呢?這是因為我們第一次發出請求的時候,服務器處理完了之后就將文件描述符關掉了,而鏈接的時候使用的文件描述符已經被關了,此時會鏈接失敗,所以要想再次鏈接,就必須要再次創建套接字。

此時就解決問題啦!

6.客戶端未發送,服務器斷開

當客戶端向服務器寫的時候,此時服務器斷開了,此時我們希望能夠重新連接服務器,如果重連5次還沒有連接上,用戶也離線。

#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>using namespace std;
void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// tcp客戶端要不要bind?一定要顯示綁定只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送connect的時候,進行自動隨機綁定// 可是客戶端此時不知道服務器的ip和端口號// 使用命令行參數來解決struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){int cnt = 5; // 重連的次數bool isreconnect = false;int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;}do{int n = connect(sockfd, (const struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "connect error..., reconnect: " << cnt << std::endl;close(sockfd);sleep(2);}else{break;}} while (cnt && isreconnect);if (cnt == 0){std::cerr << "user offline..." << std::endl;break;}string message;cout << "Please Enter# ";getline(cin, message);// 發送數據n = write(sockfd, message.c_str(), message.size());if (n < 0){isreconnect = true;std::cerr << "write error..." << std::endl;continue;}// 讀取數據char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}close(sockfd);}return 0;
}

運行結果:

上面這個情況就是我們打游戲的時候,我們的網斷開了,就相當于連接不上服務器了,此時就會多次連接,如果連接了很多次都沒有連接上,此時游戲也就會退出啦。

五、英譯漢服務器

makefile

.PHONY:all
all:tcpserver tcpclient
tcpserver:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11 .PHONY:clean
clean:rm -f tcpserver tcpclient

dict.txt

apple:蘋果...
banana:香蕉...
red:紅色...
yellow:黃色...
the: 這
be: 是
to: 朝向/給/對
and: 和
I: 我
in: 在...里
that: 那個
have: 有
will: 將
for: 為了
but: 但是
as: 像...一樣
what: 什么
so: 因此
he: 他
her: 她
his: 他的
they: 他們
we: 我們
their: 他們的
his: 它的
with: 和...一起
she: 她
he: 他(賓格)
it: 它

Main.cc

#include "TcpServer.hpp"#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// ./tcpserver 8080
int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(UsageError);}uint16_t port = std::stoi(argv[1]);unique_ptr<TcpServer> tcpSvr(new TcpServer(port));tcpSvr->Init();tcpSvr->Start();return 0;
}

Task.hpp

#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "Init.hpp"
#include <string.h>extern Log lg;
Init init;
using namespace std;class Task
{
public:Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}void run(){char buffer[4096];// 讀取用戶發送的請求ssize_t n = read(sockfd_, buffer, sizeof(buffer));if (n > 0){buffer[n] = '\0';cout << "client keys: " << buffer << endl;string echo_string = init.translation(buffer);int n = write(sockfd_, echo_string.c_str(), echo_string.size());if(n < 0){lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));}}else if (n == 0){// 此時需要關閉文件描述符lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else{lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));}close(sockfd_);}void operator()(){run();}~Task(){}private:int sockfd_;std::string clientip_;uint16_t clientport_;
};

Init.pp

#pragma once#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "log.hpp"Log lg;const std::string dictname = "./dict.txt";
const std::string sep = ":";static bool Split(std::string &s, std::string *part1, std::string *part2)
{auto pos = s.find(sep);if (pos == std::string::npos)return false;*part1 = s.substr(0, pos);*part2 = s.substr(pos + 1);return true;
}class Init
{
public:Init(){std::ifstream in(dictname); // 打開配置文件if (!in.is_open())          // 打開配置文件失敗{lg(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}std::string line;while (std::getline(in, line)){std::string part1, part2;Split(line, &part1, &part2);dict.insert({part1, part2});}in.close();}std::string translation(const std::string &key){auto iter = dict.find(key);if (iter == dict.end())return "Unknow";elsereturn iter->second;}private:std::unordered_map<std::string, std::string> dict;
};

log.hpp

#pragma once
#include <iostream>
#include <string>
#include "log.hpp"
#include "Init.hpp"
#include <string.h>extern Log lg;
Init init;
using namespace std;class Task
{
public:Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}void run(){char buffer[4096];// 讀取用戶發送的請求ssize_t n = read(sockfd_, buffer, sizeof(buffer));if (n > 0){buffer[n] = '\0';cout << "client keys: " << buffer << endl;string echo_string = init.translation(buffer);int n = write(sockfd_, echo_string.c_str(), echo_string.size());if(n < 0){lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));}}else if (n == 0){// 此時需要關閉文件描述符lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else{lg(Warning, "read error, errno : %d, errstring: %s", errno, strerror(errno));}close(sockfd_);}void operator()(){run();}~Task(){}private:int sockfd_;std::string clientip_;uint16_t clientport_;
};

TcpServer.hpp

#pragma onec#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
using namespace std;const int defaultsockfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要設置的太大
extern Log lg;enum
{UsageError = 1,SocketError = 2,BindError = 3,ListenError = 4
};class TcpServer;class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;string clientip;uint16_t clientport;TcpServer *tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t &port, const string &ip = defaultip): _listensocket(defaultsockfd), _port(port), _ip(ip){}void Init(){// 1.創建套接字_listensocket = socket(AF_INET, SOCK_STREAM, 0);if (_listensocket < 0) // 創建套接字失敗{lg(Fatal, "create socket error, errno : %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket success, listensocket: %d", _listensocket);// 2.綁定端口號//  使用這個結構體需要包頭文件struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;// 轉化為網絡序列local.sin_port = htons(_port);// 字符串轉化為點時分形式的ipinet_aton(_ip.c_str(), &(local.sin_addr));// local.sin_addr.s_addr = INADDR_ANY;// 此時我們僅僅只在用戶棧填好了,并沒有寫進到打開的網絡文件和套接字中,沒有設置系統中int n = bind(_listensocket, (const struct sockaddr *)&local, sizeof(local));if (n < 0){lg(Fatal, "bind error, errno : %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket success, listensocket: %d", _listensocket);// 3.設置監聽狀態// Tcp是面向連接的,服務器一般是比較“被動的”,服務器一直處于一種,一直在等待連接到來的狀態if (listen(_listensocket, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "bind socket success, listensocket: %d", _listensocket);}void Start(){signal(SIGPIPE, SIG_IGN);ThreadPool<Task>::GetInstance()->Start();lg(Info, "tcpServer is running....");for (;;){// 1. 獲取新連接 - 知道客戶端的ip地址和端口號struct sockaddr_in client;socklen_t len = sizeof(client);// _sockfd的核心工作是: 從底層獲取客戶端的請求 - 餐廳門口的迎賓// sockdf的核心工作是: 處理會去來的客戶請求 - 餐廳的服務員int sockfd = accept(_listensocket, (struct sockaddr *)&client, &len);if (sockfd < 0){// 從底層獲取客戶端的請求 - 餐廳門口的迎賓 - 路人不來吃飯 - 換一下批路人lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?continue;                                                                      // 所以這里使用continue}lg(Info, "get a new link..., sockfd: %d", sockfd);// 2.根據新連接來進行通信// 獲取客戶端的ip地址和端口號uint16_t clientport = ntohs(client.sin_port);char clientip[32];// 轉化成主機序列inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));cout << "clientport: " << clientport << ", clientip: " << clientip << endl;// 單進程版本// Service(sockfd, clientip, clientport);// close(sockfd);// 多進程版本// pid_t id = fork();// if(id == 0)//{// 子進程// 子進程會繼承父進程的文件描述符// close(_listensocket);// if(fork() > 0) exit(0); // 此時子進程退出了// Service(sockfd, clientip, clientport); // 孫子進程執行// 對于孫子進程,它的父進程已經退出了,此時孫子進程被系統領養// close(sockfd);// exit(0);//}// 父進程// 文件描述符使用的是引用計數// 關閉父進程的文件描述符不會影響子進程// close(sockfd);// 這里等待回收子進程的方式不能是阻塞等待// pid_t rid = waitpid(id, nullptr, 0);// 多線程版本// ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);// pthread_t tid;// pthread_create(&tid, nullptr, Rountine, td);// 這里不用join,因為它是阻塞等待// pthread_join(tid, nullptr);// 線程池版本Task t(sockfd, clientip, clientport);// 單例模式ThreadPool<Task>::GetInstance()->Push(t);}}//static void* Rountine(void* args)//{// pthread_detach(pthread_self()); // 設置分離狀態// 文件描述符共享,此時我們就不能關閉// 多線程只擁有tcb//ThreadData *td = static_cast<ThreadData *>(args);// static靜態成員方法無法使用非靜態成員方法和成員// 1.將Service放到TcpServer類外// 2.將當前對象的this指針傳入//td->tsvr->Service(td->sockfd, td->clientip, td->clientport);//delete td;//return nullptr;//}~TcpServer(){close(_listensocket);}private:int _listensocket; // 套接字uint16_t _port;    // 端口號string _ip;        // ip地址
};

TcpClient.cc

#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>using namespace std;
void Usage(const string &proc)
{cout << "\n\rUsage: " << proc << " serverip serverport\n"<< endl;
}// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}string serverip = argv[1];uint16_t serverport = stoi(argv[2]);// 客戶端也需要有ip和端口號,這樣服務器才能找到用戶,返回用戶的請求// tcp客戶端要不要bind?一定要顯示綁定只不過不需要用戶顯示的bind!一般有OS自由隨機選擇!// 一個端口號只能被一個進程bind,對server是如此,對于client,也是如此!// 如果用戶自行綁定,有可能綁定同一個端口號,導致應用無法運行// 其實client的port是多少,其實不重要,只要能保證主機上的唯一性就可以!// 未來是用戶給服務器,一旦綁定,用戶的端口號是先發給服務端,此時服務器就知道是誰了!// 但是server的port需要唯一確定,用戶要訪問服務器,隨機變化導致第一天還可以,后面無法運行// 系統什么時候給我bind呢?首次發送connect的時候,進行自動隨機綁定// 可是客戶端此時不知道服務器的ip和端口號// 使用命令行參數來解決struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));while (true){int cnt = 5; // 重連的次數bool isreconnect = false;int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){cerr << "socket error" << endl;}do{int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "connect error..., reconnect: " << cnt << std::endl;sleep(2);}else{break;}} while (cnt && isreconnect);if (cnt == 0){std::cerr << "user offline..." << std::endl;break;}string message;cout << "Please Enter# ";getline(cin, message);// 發送數據int n = write(sockfd, message.c_str(), message.size());if (n < 0){isreconnect = true;std::cerr << "write error..." << std::endl;continue;}// 讀取數據char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}close(sockfd);}return 0;
}

服務端:

客戶端:

然后我們再來測試一下如果服務器斷開了我們還能不能重新連上。

我們發現此時不能重現連接成功,這是因為我們的端口號不能重現啟動,所以我們要在服務器端加兩句代碼。

int opt = 1;
setsockopt(_listensocket, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt)); 
// 防止偶發性的服務器無法進行立即重啟(tcp協議的時候再說)

此時我們來看看運行結果:

六、前臺和后臺進程

可是萬一有一天我們不小心將我們的xshell關掉了呢?此時服務器就斷開了,我們想xshell關掉了服務器依然能跑,要做到這個,我們要理解前臺進程和后臺進程,先來測試一下它們的特點。

#include <iostream>
#include <string>
#include <unistd.h>int main()
{while(true){std::cout << "hello ...." << std::endl;sleep(1);}return 0;
}

前臺進程:

后臺進程:

前臺進程

  1. 直接交互:前臺進程直接與用戶交互,意味著用戶可以通過命令行與這些進程進行輸入和輸出操作。
  2. 終端阻塞:當一個進程在前臺運行時,它通常會阻塞用戶終端,直到該進程完成或被掛起。這意味著用戶不能在同一終端啟動其他需要交互的進程。
  3. 數量限制:在一個會話中,同時只能有一個進程組在前臺運行,盡管這個進程組內可能包含多個進程。

后臺進程

  1. 非交互式運行:后臺進程在后臺運行,不直接與用戶交互,即使用戶沒有主動與其互動,也能持續執行任務。
  2. 不阻塞終端:用戶可以在后臺進程運行的同時,在同一個終端上執行其他命令或啟動其他進程,因為它不阻塞用戶界面。
  3. 啟動方式:可以通過在命令末尾添加符號&來將進程置于后臺啟動。

此時我們可以把進程運行的結果重定向到文件中,如果交給我們的前臺進程,那么此時只能執行一個寫入文件操作,因為我們的前臺進程只有一個,但是后臺進程有多個,我們可以交給后臺進程,并且還能通過jobs來查看后臺進程。

如果我們向終止任務,可以使用fg 任務號將這個任務提到前臺進程,但是此時bash就會變成后臺進程,然后將它干掉即可。如果我們不想干掉,想重新仍會后臺進程呢?

此時我們暫停前臺進程,系統要不要把bash提到前臺進程,把暫停的進程提到后臺進程,要的,bash必須變成前臺進程,所以在命令行中,前臺進程一定存在。

隨后我們再來理解一下后臺進程。

每次登錄的時候,我們的session都是不同的,所以session id也是不同的。

這里我們在開啟兩個終端。

如果我們的前臺進程退了,開啟的后臺進程呢?它會自己退嘛?

此時我們發現后臺進程也退出了,所以用戶在退出的時候,會將自己啟動的所有進程關掉,這就是注銷,如果我們不想讓我們的后臺進程不隨任何用戶的登錄或者退出受影響,此時我們就要守護進程化,我們將自成session自成進程組的進程,稱為守護進程。

注意:如果調用進程不是一個進程組組長,則創建一個新的會話,但是我們怎么保證不是進程組組長的呢?我們可以創建一個子進程,然后父進程退出,此時的子進程就不是一個進程組組長,則創建一個新的會話,所以守護進程的本質,也是孤兒進程!但是該孤兒進程很堅強,它把自己設置成新的session,它就是一個獨立的session,這樣不隸屬于任何用戶登錄和注銷的影響。

#pragma onec#include <unistd.h>
#include <iostream>
#include <signal.h>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";// 讓服務器調用還函數,以守護進程的形式進行運行
void Daemon(const std::string &cwd = "")
{// 1.忽略其他異常信號signal(SIGCLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2.將自己變成獨立的會話// 父進程退出if(fork() > 0) exit(0);setsid(); // 3. 更改當前調用進程的工作目錄if (!cwd.empty())chdir(cwd.c_str());// 4.關閉? 標準輸入,標準輸出,標準錯誤// 但是如果我們關閉了,那此時日志里面的打印全都會出錯// 系統為我們提供了一個/dev/null文件,它像一個垃圾桶// 凡是向/dev/null文件里面寫入的信息全部都會被丟棄// 4.標準輸入,標準輸出,標準錯誤 -> 重定向到/dev/null文件// 此時日志的信息都會丟棄,那么我們看不到錯誤的信息了嘛?是滴// 所以日志的信息我們可以將它寫到文件中,反正不應該出現在顯示器文件int fd = open(nullfile.c_str(), O_RDWR); // 讀寫方式打開if(fd > 0) // 打開成功{dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

隨后我們將守護進程的代碼加入服務器啟動的最開始的地方。

隨后我們開始運行一下:

并且此時我們能夠確定我們的服務器的進程不隸屬于bash的session,而是單成session。

bash的session id和我們的服務器的session id不一樣哦!

此時我們能夠發現我們已經將日志重定向到垃圾桶啦!隨后我們再將我們的xshell關掉,然后我們再打開xshell,運行我們的服務器,看看結果:

此時只要我們一運行我們的客戶端,就可以訪問,真正做到了24小時提供服務,那我們怎么關掉我們的服務器呢?直接kill就行啦!!!

注意:為了標識守護進程,我們一般給文件名為末尾加上d。

如果我們想保留日志文件,那么我們就要傳入寫入的方式。

此時我們就能在日志里面看到該日志文件,但是守護進程還是太麻煩了,要自己來寫,其實系統也為我們實現了。

這個函數接受兩個整型參數:

  1. nochdir:

    • 如果?nochdir?參數為0,daemon()?函數將會把當前工作目錄更改為根目錄("/")。這是守護進程的標準行為,避免因當前工作目錄被卸載而導致的問題。
    • 如果?nochdir?為非0值,則不改變當前工作目錄。
  2. noclose:

    • 如果?noclose?參數為0,daemon()?函數會關閉標準輸入、標準輸出和標準錯誤,并將它們都重定向到?/dev/null。這可以防止守護進程因為試圖寫入終端而阻塞或產生不必要的輸出。
    • 如果?noclose?為非0值,標準輸入、輸出和錯誤保持不變。但通常情況下,為了確保守護進程的無終端運行,我們會選擇關閉它們。

使用 daemon() 函數的基本步驟通常包括:

  • 調用?fork()?創建子進程,父進程退出,這樣新進程就不再與終端關聯。
  • 在子進程中調用?setsid()?成為新的會話領導并脫離控制終端。
  • 調用?umask()?設置合適的權限掩碼。
  • 根據需要調用?chdir("/")?更改當前工作目錄到根目錄。
  • 重定向標準輸入、輸出和錯誤流,或者通過?daemon()?函數自動處理。
  • 繼續執行守護進程的具體任務。

七、TCP協議通訊流程

下圖是基于TCP協議的客戶端/服務器程序的一般流程:

服務器初始化:

  • 調用socket, 創建文件描述符;
  • 調用bind, 將當前的文件描述符和ip/port綁定在一起;
  • 如果這個端口已經被其他進程占用了, 就會bind失敗;
  • 調用listen, 聲明當前這個文件描述符作為一個服務器的文件描述符, 為后面的accept做好準備; 調用accecpt, 并阻塞, 等待客戶端連接過來;

建立連接的過程:

  • 調用socket, 創建文件描述符;
  • 調用connect, 向服務器發起連接請求;
  • connect會發出SYN段并阻塞等待服務器應答;
  • (第一次) 服務器收到客戶端的SYN, 會應答一個SYN-ACK段表示"同意建立連接"; (
  • 第二次) 客戶端收到SYN-ACK后會從connect()返回, 同時應答一個ACK段; (第三次)

這個建立連接的過程, 通常稱為 三次握手;

數據傳輸的過程

  • 建立連接后,TCP協議提供全雙工的通信服務;
  • 所謂全雙工的意思是, 在同一條連接中, 同一時刻, 通信雙方 可以同時寫數據;
  • 相對的概念叫做半雙工, 同一條連接在同一時刻, 只能由一方來寫數據;
  • 服務器從accept()返回后立刻調 用read(), 讀socket就像讀管道一樣, 如果沒有數據到達就阻塞等待;
  • 這時客戶端調用write()發送請求給服務器, 服務器收到后從read()返回,對客戶端的請求進行處理, 在此期 間客戶端調用read()阻塞等待服務器的應答;
  • 服務器調用write()將處理結果發回給客戶端, 再次調用read()阻塞等待下一條請求;
  • 客戶端收到后從read()返回, 發送下一條請求,如此循環下去;

斷開連接的過程:

  • 如果客戶端沒有更多的請求了, 就調用close()關閉連接, 客戶端會向服務器發送FIN段(第一次);
  • 此時服務器收到FIN后, 會回應一個ACK, 同時read會返回0 (第二次);
  • read返回之后, 服務器就知道客戶端關閉了連接, 也調用close關閉連接, 這個時候服務器會向客戶端發送 一個FIN;(第三次)
  • 客戶端收到FIN, 再返回一個ACK給服務器; (第四次)

這個斷開連接的過程, 通常稱為 四次揮手

在學習socket API時要注意應用程序和TCP協議層是如何交互的:

  • 應用程序調用某個socket函數時TCP協議層完成什么動作,比如調用connect()會發出SYN段
  • 應用程序如何知道TCP協議層的狀態變化,比如從某個阻塞的socket函數返回就表明TCP協議收到了某些 段,再比如read()返回0就表明收到了FIN段

談戀愛例子

八、TCP全雙工通信

全雙工通信

  • 在全雙工模式下,數據可以同時在兩個方向上傳輸,即通信的雙方能夠同時進行發送和接收操作,互不影響。這就像兩個人在打電話,雙方可以同時說話和聆聽,無需等待對方說完再回應。

半雙工通信

  • 半雙工模式允許數據在兩個方向上傳輸,但不能同時進行。這意味著在任何給定的時間點,數據只能在一個方向流動。一旦一方開始發送數據,另一方就必須停止發送并轉為接收模式,直到前一方發送完畢。這種模式類似于對講機,使用者必須等待對方講完并說“Over”后,才可開始自己的講話。

我們為什么要講這個呢?因為我們的tcp是全雙工通信的,它是如何做到的呢?

每個TCP連接都有獨立的發送緩沖區和接收緩沖區。這意味著一個端點可以在其發送緩沖區排隊待發送的數據,同時從接收緩沖區讀取對方發送過來的數據。這兩個操作可以并發進行,從而實現了數據的雙向同時傳輸,所以未來我們可以對一個套接字多進程的并發的讀和寫,但是兩個線程不能同時讀和同時寫。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/13829.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/13829.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/13829.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AI大模型日報#0523:中國大模型價格戰的真相、大模型「上車」、王小川首款 AI 應用

導讀&#xff1a;AI大模型日報&#xff0c;爬蟲LLM自動生成&#xff0c;一文覽盡每日AI大模型要點資訊&#xff01;目前采用“文心一言”&#xff08;ERNIE 4.0&#xff09;、“零一萬物”&#xff08;Yi-Large&#xff09;生成了今日要點以及每條資訊的摘要。歡迎閱讀&#xf…

04. Redis 配置文件

文章目錄 單位包含網絡 NETWORK通用 GENERAL快照 SNAPSHOTTING主從復制 REPLICATION安全 SECURITY客戶端 CLIENTS內存設置 MEMORY MANAGEMENTAPPEND ONLY MODE 模式&#xff08;aof 的配置&#xff09; 單位 配置文件對大小寫不敏感&#xff08;unit單位&#xff09;。 包含 …

Flutter 中的 WillPopScope 小部件:全面指南

Flutter 中的 WillPopScope 小部件&#xff1a;全面指南 在 Flutter 應用開發中&#xff0c;WillPopScope 是一個非常有用的小部件&#xff0c;它允許開發者攔截和處理用戶嘗試退出當前頁面的操作。這在需要確認用戶是否真的想要離開當前頁面&#xff0c;或者在離開前需要執行…

京東h5st加密參數分析與批量商品價格爬取(文末含純算法)

文章目錄 1. 寫在前面2. 接口分析3. 加密分析4. 算法還原【??作者主頁】:吳秋霖 【??作者介紹】:擅長爬蟲與JS加密逆向分析!Python領域優質創作者、CSDN博客專家、阿里云博客專家、華為云享專家。一路走來長期堅守并致力于Python與爬蟲領域研究與開發工作! 【??作者推…

羅德與施瓦茨ZNB20矢量網絡分析儀怎么讀取Trace?

矢量網絡分析儀(VNA)是電子測量領域廣泛應用的重要儀器&#xff0c;可以幫助工程師精確測量各種射頻和微波設備的參數&#xff0c;為設計優化、故障診斷等提供關鍵數據支持。作為業界領先的VNA制造商&#xff0c;羅德與施瓦茨的ZNB20型號在測量精度、動態范圍、掃描速度等方面都…

家政預約小程序05服務管理

目錄 1 設計數據源2 后臺管理3 后端API4 調用API總結 家政預約小程序的核心是展示家政公司提供的各項服務的能力&#xff0c;比如房屋維護修繕&#xff0c;家電維修&#xff0c;育嬰&#xff0c;日常保潔等。用戶在選擇家政服務的時候&#xff0c;價格&#xff0c;評價是影響用…

中國網對話神工坊創始人任虎: 先進計算技術賦能,領跑自主CAE新時代

隨著"中國制造2025"收官在即&#xff0c;智能制造和工業互聯網的發展勢頭更勁。作為現代工業的基石&#xff0c;工業軟件已成為推動工業數字化轉型的關鍵力量。 近日&#xff0c;神工坊創始人&CEO任虎先生接受了中國網記者的專訪&#xff0c;就“國產CAE軟件的崛…

C++中的Lambda的定義與使用

文章目錄 前言Lambda的定義與使用方式總結 Lambda的使用和細節 前言 在C11引入了Lambda表達式&#xff0c;它是一種方便的匿名函數&#xff0c;可以在需要時臨時定義函數&#xff0c;并且可以捕獲局部變量。下面是Lambda表達式的定義與使用方式&#xff0c;并對其進行總結 La…

【東山派Vision K510開發板試用筆記】nncase的安裝

概述 最近試用了百問網提供的東山派Vision開發板&#xff0c;DongshanPI-Vision開發板是百問網針對AI應用開發設計出來的一個RSIC-V架構的AI開發板&#xff0c;主要用于學習使用嘉楠的K510芯片進行Linux項目開發和嵌入式AI應用開發等用途。DongshanPI-Vision開發板采用嘉楠公司…

持續總結中!2024年面試必問 20 道 Redis面試題(三)

上一篇地址&#xff1a;持續總結中&#xff01;2024年面試必問 20 道 Redis面試題&#xff08;二&#xff09;-CSDN博客 五、Redis的持久化機制是什么&#xff1f;各自的優缺點&#xff1f; Redis的持久化機制主要有三種&#xff1a;RDB持久化、AOF持久化以及混合持久化。下面…

Android 13 QSSI和TARGET編譯時間不一致導致recovery升級失敗

環境 $ cat /etc/os-release NAME"Ubuntu" VERSION"20.04.4 LTS (Focal Fossa)" IDubuntu ID_LIKEdebian PRETTY_NAME"Ubuntu 20.04.4 LTS" VERSION_ID"20.04" HOME_URL"https://www.ubuntu.com/" SUPPORT_URL"https:/…

寡姐不高興了:這次可能會讓 OpenAI 遇到真正的麻煩|TodayAI

寡姐這次真不高興了 演員斯嘉麗約翰遜&#xff08;Scarlett Johansson&#xff09;近日表示&#xff0c;她拒絕了 OpenAI 的邀請&#xff0c;不愿為對話式 ChatGPT 系統配音&#xff0c;卻發現公司仍然使用了一個聽起來非常像她的聲音。對此&#xff0c;她感到“震驚”和“憤怒…

react狀態管理

狀態管理的實現 組件之外&#xff0c;可以在全局共享狀態/數據 closure&#xff08;閉包&#xff09; 可以解決 有修改這個數據的明確方法&#xff0c;并且&#xff0c;能夠讓其他的方法感知到。 本質上&#xff0c;就是把監聽函數放在一個地方&#xff0c;必要時拿出來執行一…

Java數據結構與算法(最小棧)

前言 設計一個支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常數時間內檢索到最小元素的棧。 實現 MinStack 類: MinStack() 初始化堆棧對象。void push(int val) 將元素val推入堆棧。void pop() 刪除堆棧頂部的元素。int top() 獲取堆棧頂部的元素。i…

Educational Codeforces Round 160 C. Game with Multiset (貪心之盡量選最大來填滿)

在這個問題中&#xff0c;最初會給你一個空的多集。您必須處理兩種類型的查詢&#xff1a; ADD x x x - 在多集合中添加一個等于 2 x 2x 2x 的元素&#xff1b; GET w w w - 詢問是否可以求當前多集的某個子集的和&#xff0c;并得到等于 w w w 的值。 輸入 第一行包含一…

java后端輪播圖的設計

對于表示輪播圖位置這種有限且較小范圍的數據&#xff0c;一般可以使用整數類型來表示。考慮到位置序號一般是非負整數且數量較少&#xff0c;可以選擇使用小范圍的整數類型&#xff0c;如下&#xff1a; 整數類型: 對于Java中&#xff0c;可以考慮使用 int 類型來表示位置序號…

Vue3 ts實現將assets中的圖片轉為file格式,實現本地圖片選擇上傳功能

Vue3 ts實現將assets中的圖片轉為file格式&#xff0c;實現本地圖片選擇上傳功能 1、需求描述2、關鍵代碼3、img標簽src使用變量打包后圖片無法展示 1、需求描述 用戶可以選項系統固定的幾個圖標&#xff0c;也可以自定義上傳圖片。系統固定圖標存在 src\assets\images\app 路徑…

sql注入——時間盲注

在sql注入的第九關中&#xff0c;我們既看不到返回值&#xff0c;也不能通過布爾盲注得到結果&#xff0c;這個時候還有一種方法就是通過頁面反應時間來獲取信息&#xff0c;就是時間盲注 第九關的代碼&#xff0c;可以看到無論是否正確&#xff0c;頁面都會返回You are in 可…

4---git命令詳解第一部分

一、提交文件方面命令&#xff1a; 1.1第一步&#xff1a;將需要提交的文件放進暫存區&#xff1a; 添加單個文件到暫存區stage&#xff1a; git add 文件名 添加多個文件到暫存區&#xff1a; git add 文件名1 文件名2 ... 將目錄下所有文件添加到暫存區&#xff1a; git…

【漏洞復現】用友U8 CRM uploadfile 文件上傳致RCE漏洞

0x01 產品簡介 用友U8 Cloud是用友推出的新一代云ERP&#xff0c;主要聚焦成長型、創新型企業&#xff0c;提供企業級云ERP整體解決方案。 0x02 漏洞概述 用友 U8 CRM客戶關系管理系統 uploadfle.php 文件存在任意文件上傳漏洞&#xff0c;未經身份驗證的攻擊者通過漏洞上傳…