需求以及思路
客戶端啟動以後,需要去連線服務端,並在控制檯輸入訊息傳送到服務端,服務端收到該訊息後傳送給所有已連線的客戶端。
所以客戶端需要做的事情只有兩個:
- 接收使用者輸入並將其傳送到服務端
- 接收服務端訊息並將其顯示到控制檯
服務端要做的事情也是兩個:
- 接待新連線上的客戶端,為其分配一個服務者
- 接收每個客戶端的訊息,並將其傳送給所有客戶端
透過分析以上的需求,很容易得出一個簡單的實現思路,使用多執行緒或者多程序來實現。
- 客戶端連線到服務端之後,建立一個執行緒來處理服務端的訊息,然後在主執行緒中處理控制檯輸入。
- 服務端在主執行緒中監聽請求,當有客戶端連線時分配一個執行緒用於服務該客戶端。
- 並且需要記錄所有已連線的客戶端,以便能夠給所有客戶端傳送訊息。
這種方式很簡單粗暴,每次連線上一個客戶端時服務端就會新建一個執行緒或者程序,這並不是很理智的一種做法。
使用多執行緒實現
客戶端
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include "thread"
int main(){
std::atomic_bool stop = false;
// 服務端地址
struct sockaddr_in serv_addr{};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
//建立socket並連線到服務端
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))<0){
std::cerr<<"connect failed."<<std::endl;
return -1;
}
//連線建立後,建立一個執行緒讀取資料
std::thread t([&](){
thread_local char buffer[1024];
while(!stop){
memset(buffer,0,sizeof (buffer));
ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
if(bytes_len==0){
std::cout<<"server closed."<<std::endl;
break;
}else if(bytes_len<0){
std::cout<<"recv error. exit!"<<std::endl;
break;
}else{
std::cout<<buffer<<std::endl;
}
}
stop = true;
exit(0);
});
//主執行緒中輸入併傳送
while(!stop){
char buffer[1024];
std::cin>>buffer;
if(stop||strcmp(buffer,"quit")==0){
break;
}
ssize_t bytes_len = send(fd, buffer, sizeof(buffer),0);
if(bytes_len<0)
{
std::cout<<"send error. exit!"<<std::endl;
break;
}
}
stop = true;
t.join();
close(fd);
return 0;
}
服務端
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <list>
#include <thread>
std::atomic_bool stop = false;
std::mutex mu;
// 儲存所有客戶端檔案描述符的容器
std::vector<int> clientFdVec;
// 儲存所有服務執行緒的容器
std::vector<std::thread> clientWorkerVec;
/**
* @brief notify 傳送訊息給某個指定的客戶端或者所有的客戶端
* @param msg
* @param receiverFd -1表示傳送給所有客戶端,若要傳送給指定客戶端,該值需要>1
* @param exceptFd receiverFd為-1時生效,即不傳送給某客戶端
*/
void notify(const std::string & msg,int receiverFd=-1,int exceptFd=-1){
if(receiverFd<0){
std::lock_guard<std::mutex> lk(mu);
for(int cfd : clientFdVec){
if(cfd==exceptFd) {
continue;
}
send(cfd,msg.c_str(),msg.length(),0);
}
}else{
send(receiverFd,msg.c_str(),msg.length(),0);
}
}
/**
* @brief 客戶端服務執行緒
* @param fd
*/
void clientWork(int fd){
char buffer[1024];
while(!stop){
memset(buffer,0,sizeof(buffer));
ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
if(bytes_len==0){
std::cout<<"client "<<fd<<" closed."<<std::endl;
break;
} else if(bytes_len<0){
std::cerr<<"client "<<fd<<" recv error."<<std::endl;
break;
}else{
std::string msg = std::to_string(fd)+":"+buffer;
notify(msg);
}
}
std::lock_guard<std::mutex> lk(mu);
auto it = std::remove_if(clientFdVec.begin(), clientFdVec.end(),[&](int element){
return element == fd;
});
clientFdVec.erase(it, clientFdVec.end());
}
int main() {
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(1234);
// 建立socket
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 繫結socket和addr
if(bind(fd, (struct sockaddr *) &addr, sizeof(addr))<0){
std::cout << "bind failed!" << std::endl;
return -1;
}
// 監聽指定地址埠
while(0==listen(fd, 20)){
//接收連線請求
sockaddr_in clientAddr{};
socklen_t clientAddrLen = sizeof(clientAddr);
int clientFd = accept(fd, (struct sockaddr*)&clientAddr, &clientAddrLen);
if(clientFd<0){
std::cerr<<"accept error."<<std::endl;
break;
}
//記錄這個客戶端
{
std::lock_guard<std::mutex> lk(mu);
clientFdVec.emplace_back(clientFd);
}
//開啟一個執行緒服務這個客戶端
clientWorkerVec.emplace_back(clientWork,clientFd);
//給所有客戶端傳送訊息:welcome xxx!
notify(std::string("welcome ")+std::to_string(clientFd));
}
stop = true;
for(auto & t : clientWorkerVec){
t.join();
}
close(fd);
for(int cfd : clientFdVec){
close(cfd);
}
return 0;
}
使用select解決執行緒建立過多的問題
select基本用法
詳細用法以及原理參考 關於Select Model的兩篇譯文
//#include <sys/select.h>
void FD_CLR(fd, fd_set *fdset);
void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy);
int FD_ISSET(fd, fd_set *fdset);
void FD_SET(fd, fd_set *fdset);
void FD_ZERO(fd_set *fdset);
int select(int nfds,
fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);
簡單示例
#include <sys/select.h>
#include <cstdio>
#include <unistd.h>
int main() {
fd_set rd;
struct timeval tv{};
int ret;
FD_ZERO(&rd); //所有位置歸零
// #include <unistd.h>
// 0:STDIN_FILENO 標準輸入
// 1:STDOUT_FILENO 標準輸出
// 2:STDERR_FILENO 標準錯誤輸出
FD_SET(0, &rd); //標準輸入檔案描述符加入到rd集合中
// 設定超時時間位5s
tv.tv_sec = 3;
tv.tv_usec = 0;
// 將rd集合進行select,監聽其可讀事件
// 程式在這裡阻塞,直到超時或者標準輸入上有資料可讀
ret = select(1, &rd, nullptr, nullptr, &tv);
if (ret == 0) // Timeout
{
printf("select timeout!\n");
} else if (ret == -1) // Failure
{
printf("fail to select!\n");
} else // Successful
{
printf("data is available!\n");
char buffer[1024]{0};
read(0,buffer,sizeof(buffer));
printf("the data is [%s]!\n",buffer);
}
return 0;
}
服務端
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <list>
#include <thread>
std::atomic_bool stop = false;
int main() {
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(1234);
fd_set readFdSet;
fd_set tmpReadFdSet;
FD_ZERO(&readFdSet);
int srvFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (bind(srvFd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
std::cout << "bind failed!" << std::endl;
return -1;
}
listen(srvFd, 5);
FD_SET(srvFd, &readFdSet);
int ret;
while (!stop) {
tmpReadFdSet = readFdSet;
ret = select(FD_SETSIZE, &tmpReadFdSet, nullptr, nullptr, nullptr);
if (ret == 0) {
std::cerr << "time out." << std::endl;
break;
} else if (ret < 0) {
std::cerr << "select error." << std::endl;
break;
}
for (int fd = 0; fd < FD_SETSIZE; fd++) {
if (!FD_ISSET(fd, &tmpReadFdSet)) {
continue;
}
if (fd == srvFd) //處理server
{
struct sockaddr_in client_address{};
socklen_t client_len = sizeof(client_address);
int cfd = accept(fd, (struct sockaddr *) &client_address, &client_len);
FD_SET(cfd, &readFdSet);
std::cout << "client[" << cfd << "]connected." << std::endl;
} else //處理訊息
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_len = recv(fd, buffer, sizeof(buffer), 0);
if (bytes_len == 0) {
std::cout << "client " << fd << " closed." << std::endl;
FD_CLR(fd, &readFdSet);
std::string msg = std::to_string(fd) + " leaved.";
for (int i = 1; i < FD_SETSIZE; i++) {
if (i != srvFd && FD_ISSET(i, &readFdSet)) {
send(i, msg.c_str(), msg.length(), 0);
}
}//end for
break;
} else if (bytes_len < 0) {
std::cerr << "client " << fd << " recv error." << std::endl;
break;
} else {
//send message to all
std::string msg = std::to_string(fd) + ":" + buffer;
for (int i = 1; i < FD_SETSIZE; i++) {
if (i != srvFd && FD_ISSET(i, &readFdSet)) {
send(i, msg.c_str(), msg.length(), 0);
}
}//end for
}
}
}
}
return 0;
}
客戶端
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <iostream>
#include "thread"
int main(){
std::atomic_bool stop = false;
// 服務端地址
struct sockaddr_in serv_addr{};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
//建立socket並連線到服務端
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(connect(fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))<0){
std::cerr<<"connect failed."<<std::endl;
return -1;
}
//連線建立後,建立一個執行緒讀取資料
std::thread t([&](){
thread_local char buffer[1024];
while(!stop){
memset(buffer,0,sizeof (buffer));
ssize_t bytes_len = recv(fd,buffer,sizeof(buffer),0);
if(bytes_len==0){
std::cout<<"server closed."<<std::endl;
break;
}else if(bytes_len<0){
std::cout<<"recv error. exit!"<<std::endl;
break;
}else{
std::cout<<buffer<<std::endl;
}
}
stop = true;
exit(0);
});
//主執行緒中輸入併傳送
while(!stop){
char buffer[1024];
std::cin>>buffer;
if(stop||strcmp(buffer,"quit")==0){
break;
}
ssize_t bytes_len = send(fd, buffer, sizeof(buffer),0);
if(bytes_len<0)
{
std::cout<<"send error. exit!"<<std::endl;
break;
}
}
stop = true;
t.join();
close(fd);
return 0;
}