??預(yù)備知識(shí)
?? Accept函數(shù)
accept 函數(shù)是在服務(wù)器端用于接受客戶端連接請(qǐng)求的函數(shù),它在監(jiān)聽(tīng)套接字上等待客戶端的連接,并在有新的連接請(qǐng)求到來(lái)時(shí)創(chuàng)建一個(gè)新的套接字用于與該客戶端通信。
- 下面是 accept 函數(shù)的詳細(xì)介紹以及各個(gè)參數(shù)的意義:
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: 是服務(wù)器監(jiān)聽(tīng)套接字的文件描述符,通常是使用 socket 函數(shù)創(chuàng)建的套接字。accept 函數(shù)在該套接字上等待連接請(qǐng)求。
addr: 是一個(gè)指向 struct sockaddr 類(lèi)型的指針,用于存儲(chǔ)客戶端的地址信息。當(dāng)新連接建立成功后,客戶端的地址信息將會(huì)被填充到這個(gè)結(jié)構(gòu)體中。
addrlen: 是一個(gè)指向 socklen_t 類(lèi)型的指針,它指示 addr 結(jié)構(gòu)體的長(zhǎng)度。在調(diào)用 accept 函數(shù)之前,需要將其初始化為 addr 結(jié)構(gòu)體的大小,函數(shù)執(zhí)行后會(huì)更新為實(shí)際的客戶端地址長(zhǎng)度。
返回值:如果連接成功建立,accept 函數(shù)將返回一個(gè)新的文件描述符,該文件描述符用于與客戶端進(jìn)行通信。如果連接失敗,函數(shù)將返回 -1,并設(shè)置 errno 以指示錯(cuò)誤原因。
- accept 函數(shù)的工作原理如下:
當(dāng)服務(wù)器的監(jiān)聽(tīng)套接字接收到一個(gè)新的連接請(qǐng)求時(shí),accept 函數(shù)會(huì)創(chuàng)建一個(gè)新的套接字用于與該客戶端通信。
新的套接字會(huì)繼承監(jiān)聽(tīng)套接字的監(jiān)聽(tīng)屬性,包括 IP 地址、端口等。
accept 函數(shù)會(huì)填充 addr 結(jié)構(gòu)體,以便獲取客戶端的地址信息。
服務(wù)器可以使用返回的新套接字與客戶端進(jìn)行通信。
- 注意事項(xiàng):
accept 函數(shù)在沒(méi)有連接請(qǐng)求時(shí)會(huì)阻塞,直到有新的連接請(qǐng)求到來(lái)。
如果希望設(shè)置非阻塞模式,可以使用 fcntl 函數(shù)設(shè)置 O_NONBLOCK 屬性。
在多線程或多進(jìn)程環(huán)境下,需要注意 accept 函數(shù)的線程安全性,可以使用互斥鎖等機(jī)制來(lái)保護(hù)。
綜上所述,accept 函數(shù)在構(gòu)建服務(wù)器程序時(shí)非常重要,它使服務(wù)器能夠接受客戶端的連接請(qǐng)求并創(chuàng)建新的套接字與客戶端進(jìn)行通信。
??字節(jié)序轉(zhuǎn)換函數(shù)
在網(wǎng)絡(luò)編程中,字節(jié)序問(wèn)題很重要,因?yàn)椴煌挠?jì)算機(jī)體系結(jié)構(gòu)可能使用不同的字節(jié)序,這可能導(dǎo)致在通信過(guò)程中的數(shù)據(jù)解釋錯(cuò)誤。為了在不同體系結(jié)構(gòu)之間正確傳遞數(shù)據(jù),需要進(jìn)行字節(jié)序的轉(zhuǎn)換。
- 以下是一些常用的字節(jié)序轉(zhuǎn)換函數(shù):
ntohl 和 htonl: 這些函數(shù)用于 32 位整數(shù)的字節(jié)序轉(zhuǎn)換。ntohl 用于將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為主機(jī)字節(jié)序,htonl 則相反,將主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。
ntohs 和 htons: 這些函數(shù)用于 16 位整數(shù)的字節(jié)序轉(zhuǎn)換。ntohs 用于將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為主機(jī)字節(jié)序,htons 則相反,將主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。
這些函數(shù)通常用于在網(wǎng)絡(luò)編程中處理套接字通信中的數(shù)據(jù)轉(zhuǎn)換,以確保在不同平臺(tái)上的正確數(shù)據(jù)交換。
- 示例
#include <arpa/inet.h>
int main() {
uint32_t networkValue = 0x12345678;
uint32_t hostValue = ntohl(networkValue); // 0x78563412 on a little-endian host
uint32_t convertedValue = htonl(hostValue); // 0x12345678 on a little-endian host
uint16_t networkPort = 0x1234;
uint16_t hostPort = ntohs(networkPort); // 0x3412 on a little-endian host
uint16_t convertedPort = htons(hostPort); // 0x1234 on a little-endian host
return 0;
}
請(qǐng)注意,在使用這些函數(shù)時(shí),需要包含 <arpa/inet.h> 頭文件。這些函數(shù)通常在網(wǎng)絡(luò)編程中用于正確處理字節(jié)序問(wèn)題,以確保不同平臺(tái)之間的數(shù)據(jù)傳輸正確。
??listen函數(shù)
在TCP通信中,服務(wù)端需要使用 listen 函數(shù)來(lái)監(jiān)聽(tīng)連接請(qǐng)求。這是因?yàn)門(mén)CP是一種面向連接的協(xié)議,它采用客戶端-服務(wù)端模型進(jìn)行通信,通信雙方需要先建立連接,然后進(jìn)行數(shù)據(jù)的傳輸。監(jiān)聽(tīng)的過(guò)程是為了等待客戶端發(fā)起連接請(qǐng)求。
- 具體原因如下:
建立連接: 在TCP通信中,通信雙方需要通過(guò)三次握手建立連接??蛻舳送ㄟ^(guò) connect 函數(shù)向服務(wù)器發(fā)起連接請(qǐng)求,而服務(wù)端則需要通過(guò) listen 函數(shù)來(lái)準(zhǔn)備接收連接請(qǐng)求。
處理并發(fā)連接: 服務(wù)端可能會(huì)同時(shí)接收多個(gè)客戶端的連接請(qǐng)求,而每個(gè)連接都需要為其分配一個(gè)獨(dú)立的套接字。通過(guò)監(jiān)聽(tīng)連接請(qǐng)求,服務(wù)端可以在一個(gè)循環(huán)中接受多個(gè)連接,為每個(gè)連接創(chuàng)建對(duì)應(yīng)的套接字,從而實(shí)現(xiàn)并發(fā)處理多個(gè)客戶端。
連接隊(duì)列: listen 函數(shù)將連接請(qǐng)求存儲(chǔ)在一個(gè)隊(duì)列中,等待服務(wù)端逐個(gè)接受。這個(gè)隊(duì)列稱(chēng)為“未完成連接隊(duì)列”(backlog queue)。如果連接請(qǐng)求過(guò)多,超出了隊(duì)列的長(zhǎng)度,那么新的連接請(qǐng)求可能會(huì)被拒絕或被丟棄。
連接參數(shù): listen 函數(shù)還可以指定一個(gè)參數(shù),表示在未完成連接隊(duì)列中可以容納的連接請(qǐng)求數(shù)量。這個(gè)參數(shù)可以影響服務(wù)端處理并發(fā)連接的能力。
總之,TCP監(jiān)聽(tīng)是為了等待客戶端發(fā)起連接請(qǐng)求,建立連接,然后實(shí)現(xiàn)雙方的數(shù)據(jù)傳輸。這種機(jī)制允許服務(wù)器處理多個(gè)客戶端連接,實(shí)現(xiàn)高并發(fā)的網(wǎng)絡(luò)服務(wù)。
- 函數(shù)原型:
int listen(int sockfd, int backlog);
- 參數(shù)說(shuō)明:
sockfd:要進(jìn)行監(jiān)聽(tīng)的套接字描述符。
backlog:表示在未完成連接隊(duì)列中可以容納的連接請(qǐng)求數(shù)量。這個(gè)參數(shù)可以影響服務(wù)器處理并發(fā)連接的能力。通常情況下,系統(tǒng)會(huì)為這個(gè)值設(shè)置一個(gè)默認(rèn)的最大值,但你也可以根據(jù)你的需求進(jìn)行適當(dāng)調(diào)整。
返回值:
如果函數(shù)調(diào)用成功,返回 0。
如果出現(xiàn)錯(cuò)誤,返回 -1,并設(shè)置全局變量 errno 來(lái)指示錯(cuò)誤類(lèi)型。
使用步驟:
創(chuàng)建套接字并綁定地址。
調(diào)用 listen 函數(shù)將套接字標(biāo)記為被動(dòng)套接字,開(kāi)始監(jiān)聽(tīng)連接請(qǐng)求。
使用 accept 函數(shù)接受客戶端連接請(qǐng)求,建立實(shí)際的連接。
- 示例用法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(listen_sock, 5) == -1) { // 開(kāi)始監(jiān)聽(tīng),最多允許5個(gè)未完成連接
perror("listen");
exit(EXIT_FAILURE);
}
// 現(xiàn)在可以使用 accept 函數(shù)接受連接請(qǐng)求并建立連接
close(listen_sock);
return 0;
}
注意:listen 后的套接字僅能用于接受連接請(qǐng)求,不能用于讀寫(xiě)數(shù)據(jù)。接收到的連接請(qǐng)求將在一個(gè)隊(duì)列中等待,直到使用 accept 函數(shù)從隊(duì)列中取出并建立連接。
??代碼
??Log.hpp
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
assert(level >= DEBUG);
assert(level <= FATAL);
char *name = getenv("USER");
char logInfo[1024];
va_list ap; // ap -> char*
va_start(ap, format);
vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);
va_end(ap); // ap = NULL
FILE *out = (level == FATAL) ? stderr:stdout;
fprintf(out, "%s | %u | %s | %s\n", \
log_level[level], \
(unsigned int)time(nullptr),\
name == nullptr ? "unknow":name,\
logInfo);
// char *s = format;
// while(s){
// case '%':
// if(*(s+1) == 'd') int x = va_arg(ap, int);
// break;
// }
}
??Makefile
.PHONY:all
all:TCPClient TCPServer
TCPClient: TCPClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
TCPServer:TCPServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f TCPClient TCPServer
??TCPClient.cc
#include"util.hpp"
volatile bool quit=false;
static void Usage(std::string proc)
{
std::cerr<<"Usage:\n\t"<<proc<<"serverip serverport "<<std::endl;
std::cerr<<"Example:\n\t"<<proc<<"127.0.0.1 8080\n"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);
//1.創(chuàng)建socket SOCK_STREAM
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
std::cerr<<"socket :"<<strerror(errno)<<std::endl;
exit(SOCKET_ERR);
}
//2.鏈接
//向服務(wù)器發(fā)起鏈接請(qǐng)求
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(server.sin_port);
inet_aton(serverip.c_str(),&server.sin_addr);
//2.2發(fā)起請(qǐng)求 connect自動(dòng)會(huì)進(jìn)行bind
if(connect(sock,(const struct sockaddr*)&server,sizeof(server))!=0)
{
//鏈接失敗
std::cerr<<"connect :"<<strerror(errno)<<std::endl;
exit(CONN_ERR);
}
//鏈接成功
std::cout<<" info :connect success :"<<sock<<std::endl;
std::string message;
while(!quit)
{
message.clear();
std::cout<<"請(qǐng)輸入您的消息>>>>"<<std::endl;
std::getline(std::cin,message);
if(strcasecmp(message.c_str(),"quit")==0)
{
//如果輸入的是quit 直接退出程序
quit=true; //設(shè)置成true 會(huì)把當(dāng)前信息先執(zhí)行發(fā)送到服務(wù)器 再進(jìn)入while循環(huán)時(shí)條件不滿直接退出
}
//從服務(wù)器接收到的消息
ssize_t s=write(sock,message.c_str(),message.size());
if(s>0)
{
message.resize(1024);
ssize_t s=read(sock,(char *)(message.c_str()),1024);
if(s>0)
message[s]=0;
std::cout<<"Server Echo>>>"<<"message"<<std::endl;
}
else if (s <= 0)
{
break;
}
}
close(sock);
return 0;
}
??TCPServer.cc
#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
class ServerTcp;//先聲明
class ThreadData
{
public:
uint16_t clientPort_;//客戶端端口號(hào)
std::string clientip_;//客戶端ip
int sock_;
ServerTcp *this_;
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp *ts)
: clientPort_(port), clientip_(ip), sock_(sock),this_(ts)
{}
};
class ServerTcp
{
public:
//構(gòu)造和和析構(gòu)函數(shù)
ServerTcp(uint16_t port,const std::string &ip=""):port_(port),ip_(ip),listenSock_(-1)
{}
~ServerTcp()
{}
public:
//初始化函數(shù)
void init()
{
//第一步:創(chuàng)建套接字
listenSock_=socket(PF_INET,SOCK_STREAM,0);
if(listenSock_<0)
{
//創(chuàng)建失敗
logMessage(FATAL,"socket:%s",strerror(errno)); //用日志打印錯(cuò)誤信息
exit(SOCKET_ERR);
}
//創(chuàng)建成功
logMessage(DEBUG,"sockt:%s,%d",strerror(errno),listenSock_);
//第二步 bind綁定
//2.1填充服務(wù)器信息
struct sockaddr_in local;
memset(&local,0,sizeof(local));//設(shè)置0?
/*可以確保將所有這些字段初始化為零,以避免在實(shí)際使用過(guò)程中出現(xiàn)未定義行為或不可預(yù)測(cè)的結(jié)果。*/
local.sin_family=AF_INET;
/*如果 ip_ 為空,服務(wù)器將綁定到任意可用的本地IP地址。如果 ip_ 不為空,服務(wù)器將綁定到 ip_ 所代表的具體IP地址。*/
ip_.empty()?(local.sin_addr.s_addr)=htons(INADDR_ANY):(inet_aton(ip_.c_str(),&local.sin_addr));
//2.2
if(bind(listenSock_,(const struct sockaddr*)&local,sizeof local)<0)//
{
//bind綁定失敗
logMessage(FATAL,"bind:%s",strerror(errno));
exit(BIND_ERR);
}
//綁定成功
logMessage(DEBUG,"bind:%S,%d",strerror(errno),listenSock_);
//3.監(jiān)聽(tīng)socket
if(listen(listenSock_,5)<0)
{
logMessage(FATAL,"listen:%s",strerror(errno));
exit(LISTEN_ERR);
}
//監(jiān)聽(tīng)成功
logMessage(DEBUG,"listen:%S,%d",strerror(errno),listenSock_);
//到這一步就等待運(yùn)行 等待客戶端鏈接
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); //設(shè)置線程分離
ThreadData *td = static_cast<ThreadData*>(args);
td->this_->tranService(td->sock_, td->clientip_, td->clientPort_);
delete td;
return nullptr;
}
//加載
void loop()
{
while(true)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//獲取鏈接 accept返回值??
int serviceSock=accept(listenSock_,(struct sockaddr*)&peer,&len);
if(serviceSock<0)
{
//獲取連接失敗
logMessage(WARINING,"Accept :%S[%d]",strerror(errno),serviceSock);
continue;//獲取失敗 繼續(xù)接收....
}
//獲取客戶端的基本信息 存儲(chǔ)起來(lái)
uint16_t peerPort=ntohs(peer.sin_port);
std::string peerip=inet_ntoa(peer.sin_addr);
//打印一下獲取的客戶端信息
logMessage(DEBUG,"Aceept :%s|%s[%d],socket fd :%d",strerror(errno),peerip.c_str(),peerPort,serviceSock);
// 5 提供服務(wù), echo -> 小寫(xiě) -> 大寫(xiě)
// 5.0 v0 版本 -- 單進(jìn)程 -- 一旦進(jìn)入transService,主執(zhí)行流,就無(wú)法進(jìn)行向后執(zhí)行,只能提供完畢服務(wù)之后才能進(jìn)行accept
// transService(serviceSock, peerIp, peerPort);
// 5.1 v1 版本 -- 多進(jìn)程版本 -- 父進(jìn)程打開(kāi)的文件會(huì)被子進(jìn)程繼承嗎?會(huì)的
// pid_t id = fork();
// assert(id != -1);
// if(id == 0)
// {
// close(listenSock_); //建議
// //子進(jìn)程
// transService(serviceSock, peerIp, peerPort);
// exit(0); // 進(jìn)入僵尸
// }
// // 父進(jìn)程
// close(serviceSock); //這一步是一定要做的!
// 5.1 v1.1 版本 -- 多進(jìn)程版本 -- 也是可以的
// 爺爺進(jìn)程
// pid_t id = fork();
// if(id == 0)
// {
// // 爸爸進(jìn)程
// close(listenSock_);//建議
// // 又進(jìn)行了一次fork,讓 爸爸進(jìn)程
// if(fork() > 0) exit(0);
// // 孫子進(jìn)程 -- 就沒(méi)有爸爸 -- 孤兒進(jìn)程 -- 被系統(tǒng)領(lǐng)養(yǎng) -- 回收問(wèn)題就交給了系統(tǒng)來(lái)回收
// transService(serviceSock, peerIp, peerPort);
// exit(0);
// }
// // 父進(jìn)程
// close(serviceSock); //這一步是一定要做的!
// // 爸爸進(jìn)程直接終止,立馬得到退出碼,釋放僵尸進(jìn)程狀態(tài)
// pid_t ret = waitpid(id, nullptr, 0); //就用阻塞式
// assert(ret > 0);
// (void)ret;
// 5.2 v2 版本 -- 多線程
// 這里不需要進(jìn)行關(guān)閉文件描述符嗎??不需要啦
// 多線程是會(huì)共享文件描述符表的!
ThreadData *td = new ThreadData(peerPort, peerip, serviceSock, this);
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)td);
// waitpid(); 默認(rèn)是阻塞等待!WNOHANG
// 方案1
// logMessage(DEBUG, "server 提供 service start ...");
// sleep(1);
}
}
//提供服務(wù)函數(shù) -----> 大小寫(xiě)轉(zhuǎn)換
void tranService(int sock,const std::string &clientip,uint16_t clientPort)
{
assert(sock>=0);
assert(!clientip.empty());
assert(clientPort>=1024); //1~~1024端口為系統(tǒng)端口 不可輕易更改
char inbuffer[BUFFER_SIZE];
while(true)
{
ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1); //-1是給\0留出一個(gè)位置
if(s>0)
{
inbuffer[s]='0';
if(strcasecmp(inbuffer,"quit")==0)
{
logMessage(DEBUG,"client quit----------%s[%d]",clientip.c_str(),clientPort);
break;
}
logMessage(DEBUG,"Treans Before:%s[%d]>>>%s",clientip.c_str(),clientPort,inbuffer);
//進(jìn)行大小寫(xiě)轉(zhuǎn)換
for(int i=0;i<s;i++)
{
if(isalpha(inbuffer[i])&&islower(inbuffer[i]))
{
inbuffer[i]=toupper(inbuffer[i]);
}
}
logMessage(DEBUG,"Trans after:%s[%d]>>>>%s",clientip.c_str(),clientPort,inbuffer);
write(sock,inbuffer,strlen(inbuffer));//給客戶端發(fā)送回去
}
else if(s==0)
{
// pipe: 讀端一直在讀,寫(xiě)端不寫(xiě)了,并且關(guān)閉了寫(xiě)端,讀端會(huì)如何?s == 0,代表對(duì)端關(guān)閉
// s == 0: 代表對(duì)方關(guān)閉,client 退出
logMessage(DEBUG, "client quit -- %s[%d]", clientip.c_str(), clientPort);
break;
}
else
{
logMessage(DEBUG, "%s[%d] - read: %s", clientip.c_str(), clientPort, strerror(errno));
break;
}
}
// 只要走到這里,一定是client退出了,服務(wù)到此結(jié)束
close(sock); // 如果一個(gè)進(jìn)程對(duì)應(yīng)的文件fd,打開(kāi)了沒(méi)有被歸還,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
};
static void Usage(std::string proc)
{
std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;
}
// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
if(argc != 2 && argc != 3 )
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if(argc == 3) ip = argv[2];
ServerTcp svr(port, ip);
svr.init();
svr.loop();
return 0;
}
?? util.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cassert>
#include <ctype.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024
大家可以拉下來(lái)自行測(cè)試…文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-642193.html
?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-642193.html
到了這里,關(guān)于【網(wǎng)絡(luò)編程】實(shí)現(xiàn)一個(gè)簡(jiǎn)單多線程版本TCP服務(wù)器(附源碼)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!