一、服務器的初始化
下面介紹程序中用到的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;
}
前臺進程:
后臺進程:
前臺進程:
- 直接交互:前臺進程直接與用戶交互,意味著用戶可以通過命令行與這些進程進行輸入和輸出操作。
- 終端阻塞:當一個進程在前臺運行時,它通常會阻塞用戶終端,直到該進程完成或被掛起。這意味著用戶不能在同一終端啟動其他需要交互的進程。
- 數量限制:在一個會話中,同時只能有一個進程組在前臺運行,盡管這個進程組內可能包含多個進程。
后臺進程:
- 非交互式運行:后臺進程在后臺運行,不直接與用戶交互,即使用戶沒有主動與其互動,也能持續執行任務。
- 不阻塞終端:用戶可以在后臺進程運行的同時,在同一個終端上執行其他命令或啟動其他進程,因為它不阻塞用戶界面。
- 啟動方式:可以通過在命令末尾添加符號&來將進程置于后臺啟動。
此時我們可以把進程運行的結果重定向到文件中,如果交給我們的前臺進程,那么此時只能執行一個寫入文件操作,因為我們的前臺進程只有一個,但是后臺進程有多個,我們可以交給后臺進程,并且還能通過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。
如果我們想保留日志文件,那么我們就要傳入寫入的方式。
此時我們就能在日志里面看到該日志文件,但是守護進程還是太麻煩了,要自己來寫,其實系統也為我們實現了。
這個函數接受兩個整型參數:
nochdir:
- 如果?
nochdir
?參數為0,daemon()
?函數將會把當前工作目錄更改為根目錄("/")。這是守護進程的標準行為,避免因當前工作目錄被卸載而導致的問題。- 如果?
nochdir
?為非0值,則不改變當前工作目錄。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連接都有獨立的發送緩沖區和接收緩沖區。這意味著一個端點可以在其發送緩沖區排隊待發送的數據,同時從接收緩沖區讀取對方發送過來的數據。這兩個操作可以并發進行,從而實現了數據的雙向同時傳輸,所以未來我們可以對一個套接字多進程的并發的讀和寫,但是兩個線程不能同時讀和同時寫。