TCP网络服务器
- [🐍 1.程序简洁](#🐍 1.程序简洁)
- [🦎2. 服务端ServerTcp程序介绍](#🦎2. 服务端ServerTcp程序介绍)
- 🦖3.线程池ThreadPool介绍
- [🦕 4.任务类Task介绍](#🦕 4.任务类Task介绍)
- [🐙5. 客户端Client介绍](#🐙5. 客户端Client介绍)
- 🦑6.运行结果:
- [🦐 7. 源码](#🦐 7. 源码)
-
- [🦞7.1 serverTcp.cc](#🦞7.1 serverTcp.cc)
- [🦀 7.2 ThreadPool.hpp](#🦀 7.2 ThreadPool.hpp)
- [🐡7.3 Task.hpp](#🐡7.3 Task.hpp)
- 🐠7.4ClientTcp.cc
- [🐟 7.5 Lock.hpp](#🐟 7.5 Lock.hpp)
- 🐬7.7头文件util.hpp
- [🐳7.6Makefile 文件](#🐳7.6Makefile 文件)
- [🐋7.8 日志文件 log.hpp](#🐋7.8 日志文件 log.hpp)
🐍 1.程序简洁
该程序用C++实现了一个支持并发的服务器,实现了大小写转换的功能,当客户端向服务器发起链接请求时,服务器响应请求,分配资源处理任务,实现大小写的转换功能,用到的相关技术栈有:C++编程、socket套接字编程、线程池等。
🦎2. 服务端ServerTcp程序介绍
在ServerTcp函数定义定义了一个init函数,用于配置服务器的网络监听套接字,绑定IP地址和端口,并开始监听来自客户端的连接请求。
①首先创建套接字:
使用 函数创建一个套接字,这里使用的是IPv4()和TCP()协议。如果创建失败,会记录错误信息并退出程序。socketPF_INETSOCK_STREAM
②绑定地址和端口:
创建一个 结构体变量 ,它用于保存服务器的本地地址信息。struct sockaddr_inlocal
2.2. 清空 结构体,然后设置其成员变量,包括地址族(,设为 表示IPv4)、端口号(,通过 将主机字节序转换为网络字节序)、IP地址(),IP地址可以是配置的服务器IP或者通配地址 。localsin_familyPF_INETsin_porthtonssin_addr.s_addrINADDR_ANY
2.3. 使用 函数将创建的套接字 与上述配置绑定。如果绑定失败,会记录错误信息并退出程序。bindlistenSock_
③监听套接字:
使用 函数将套接字设置为监听状态,同时指定了允许等待连接的队列长度为5。如果监听失败,会记录错误信息并退出程序。listen
④加载线程池:
在这里,代码通过 获取了一个线程池的单例对象,用于处理客户端的请求。这个线程池的类型是 ,这是一个自定义的任务类,它将在客户端连接时调用来处理请求。ThreadPool::getInstance()Task
最后的注释解释了程序运行到这一步时,服务器已经配置完成,等待客户端的连接请求。
这段代码是服务器的主循环函数 ,它是服务器的核心部分,用于不断接受客户端的连接请求,然后将连接的处理任务交给线程池来执行。
①打开线程池:
使用 启动线程池。这个操作会让线程池中的线程开始运行,并等待任务的到来。tp_->start()
②记录线程池启动成功的日志:
使用 记录一个 DEBUG 级别的日志,表示线程池启动成功,同时记录线程池中线程的数量。
③主循环:
进入一个无限循环,用于不断接受客户端的连接请求。
④接受连接:
使用 函数等待客户端的连接请求,当有客户端连接时, 返回一个新的套接字 ,用于与客户端通信。
⑤处理连接失败:
如果 返回的套接字小于 0,表示连接失败,代码记录一个 WARNING 级别的日志,包含错误信息,然后继续等待下一个连接请求。
⑥获取客户端信息:
6.1. 获取客户端的端口号和IP地址,并进行字节序的转换,以便后续日志记录和任务处理。
6.2. 记录 DEBUG 级别的日志,表示接受到客户端的连接请求,同时包含连接信息和套接字文件描述符。
创建任务对象:
在这里,代码创建了一个 Task对象 ,这个对象用于包装客户端的套接字、IP地址、端口号以及服务处理的回调函数 。
⑦将任务加入线程池:
使用 tp_->push(t);将任务 添加到线程池中等待执行。然后不断循环
🦖3.线程池ThreadPool介绍
该代码实现了一个简单的线程池类,当创建线程池实例时,调用start函数之后会创建15个线程(预先设定),当有任务通过push接口进入任务队列时,线程会执行threadRountine()函数,首先该判断该线程任务队列里有没有任务在执行,如果有,那就进行条件等待 void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } ,线程池里支持15个线程同时执行处理任务的函数,当任务数超过15时才会出现等待,实现了多线程并发提高了任务执行效率.
🦕 4.任务类Task介绍
'Task' 类的主要作用是将任务与处理该任务的函数关联起来,使任务的处理变得可定制化。当线程池从任务队列中取出任务时,会调用 'Task' 对象的 'operator()' 函数,进而执行与任务关联的回调函数,完成任务的处理。这种方式允许灵活地定义和执行不同类型的任务。
🐙5. 客户端Client介绍
这是一个简单的客户端程序,用于与服务器建立连接并进行通信。
首先要判断运行客户端的方式必须是 接收3个参数 运行程序+ip+端口
然后绑定协议族等信息,把接收的参数封装起来,与远端的服务器发起链接请求,进行通信,当输入的内容为"quit"时退出客户端.
🦑6.运行结果:
🦐 7. 源码
🦞7.1 serverTcp.cc
cpp
#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include"Threadpool.hpp"
#include <pthread.h>
#include"Task.hpp"
//服务函数 ---->>>小写转换大写
void transService(int sock,const std::string &clientip,uint16_t clientPort)
{
assert(sock >= 0);
assert(!clientip.empty());
assert(clientPort >= 1024);
char inbuffer[BUFFER_SIZE];
while (true)
{
ssize_t s=read(sock,inbuffer,sizeof(inbuffer)-1); //给"\0"留一个位置
if(s>0)
{
//读取成功
inbuffer[s]='\0';
if(strcasecmp(inbuffer,"quit")==0)
{
//如果输入quit直接退出
logMessage(DEBUG, "client quit -- %s[%d]", clientip.c_str(), clientPort);
break;
}
logMessage(DEBUG,"trans before: %s[%d]>>>%s",clientip.c_str(),clientPort,inbuffer);
//下面进行大小写转换
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));
}
else if (s == 0)
{
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退出了,服务到此结束
close(sock); // 如果一个进程对应的文件fd,打开了没有被归还,文件描述符泄漏!
logMessage(DEBUG, "server close %d done", sock);
}
template<class T>
class ServerTcp; // 申明一下ServerTcp
template<class T>
class ThreadData
{
public:
uint16_t clientPort_;
std::string clinetIp_;
int sock_;
ServerTcp <T>*this_;
public:
ThreadData(uint16_t port, std::string ip, int sock, ServerTcp<T> *ts)
: clientPort_(port), clinetIp_(ip), sock_(sock),this_(ts)
{}
};
template<class T>
class ServerTcp
{
public:
//构造
ServerTcp(uint16_t port,const std::string &ip=""):port_(port),ip_(ip),listenSock_(-1),tp_(nullptr)
{}
//析构
~ServerTcp()
{}
public:
void init() //初始化函数
{
//1.创建套接字
listenSock_=socket(PF_INET,SOCK_STREAM,0); //PF_INET=IPV4 SOCK_STREAM=TCP 最后一个参数是协议 默认是0
if (listenSock_ < 0) //如果socket创建成功会返回一个非负整数
{
//创建失败
logMessage(FATAL, "socket: %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);
//2.bind绑定
//2.1填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;//ipv4
local.sin_port = htons(port_);//htons 主机字节序转网络字节序
ip_.empty()?(local.sin_addr.s_addr=INADDR_ANY):(inet_aton(ip_.c_str(),&local.sin_addr));
//2.2本地socket信息 写入socket_t对应区域
if(bind(listenSock_,(const struct sockaddr*)&local,sizeof local)<0)
{
logMessage(FATAL,"bind:%s",strerror(errno));
exit(BIND_ERR);
}
logMessage(DEBUG,"bind:%s,%d",strerror(errno),listenSock_);
//3.监听套接字
if(listen(listenSock_,5)<0)
{
logMessage(FATAL,"listen:%s",strerror(errno));
exit(LISTEN_ERR);
}
logMessage(DEBUG,"listen:%s,%d",strerror(errno),listenSock_);
//加载线程池
tp_=ThreadPool<Task>::getInstance();//获取单例
//到这一步就运行起来等待客户端链接...
}
//多线程
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); //设置线程分离
ThreadData<T> *td = static_cast<ThreadData<T>*>(args);
td->this_->transService(td->sock_, td->clinetIp_, td->clientPort_);
delete td;
return nullptr;
}
void loop()
{
//打开线程池 初始化
tp_->start();
logMessage(DEBUG, "thread pool start success, thread num: %d", tp_->threadNum());
while(true)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//4.获取链接
int serviceSock=accept(listenSock_,(struct sockaddr* )&peer,&len);
if(serviceSock<0)
{
//获取链接失败
logMessage(WARINING,"Accept :%s[%d]",strerror(errno),serviceSock);
continue;//获取失败继续获取
}
//4.1获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
strerror(errno), peerIp.c_str(), peerPort, serviceSock);
//5 提供服务, echo -> 小写 -> 大写
//5.0 v0 版本 -- 单进程 -- 一旦进入transService,主执行流,就无法进行向后执行,只能提供完毕服务之后才能进行accept
//transService(serviceSock, peerIp, peerPort);
//多线版本
// ThreadData *td = new ThreadData(peerPort, peerIp, serviceSock, this);
// pthread_t tid;
// pthread_create(&tid, nullptr, threadRoutine, (void*)td);
//线程池
Task t(serviceSock, peerIp, peerPort, transService);
tp_->push(t);
}
}
private:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::string ip_;
// 引入线程池
ThreadPool<Task> *tp_;
};
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<Task> svr(port, ip);
svr.init();
svr.loop();
return 0;
}
🦀 7.2 ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <cassert>
#include <queue>
#include <memory>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "Lock.hpp"
using namespace std;
int gThreadNum = 15; // 线程数
template <class T>
class ThreadPool
{
private:
ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false)
{
assert(threadNum_ > 0);
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
// 禁止掉拷贝构造和赋值构造
ThreadPool(const ThreadPool<T> &) = delete;
void operator=(const ThreadPool<T> &) = delete;
public:
static ThreadPool<T> *getInstance()
{
static Mutex mutex;
if (nullptr == instance) // 检查是否已经存在单例对象 (第一次检查)
{
LockGuard LockGuard(&mutex); // 进入代码块 加锁 退出
if (nullptr == instance) // (第二次检查)
{
instance = new ThreadPool<T>();
}
/*首先在没有锁的情况下检查一次instance是否为空,然后在加锁的情况下再进行一次检查
确保多线程情况之下,只有一个线程成功创建实例*/
}
return instance;
}
// 线程执行函数
static void *threadRountine(void *args)
{
/*每个线程将在该函数中循环等待任务,获取任务,执行任务,然后再次等待*/
pthread_detach(pthread_self()); // 线程分离 防止资源泄漏
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
while (1)
{
tp->lockQueue();
while (!tp->haveTask())
{
tp->waitForTask();
}
// 拿到任务
T t = tp->pop();
tp->unlockQueue();
t();
}
}
void start()
{
assert(!isStart_);
for (int i = 0; i < threadNum_; i++)
{
pthread_t temp;
pthread_create(&temp, nullptr, threadRountine, this); // 创建线程
}
isStart_ = true; // 修改状态
}
void push(const T &in)
{
lockQueue();
taskQueue_.push(in);
choiceThreadForHandler();
unlockQueue();
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
int threadNum()
{
return threadNum_;
}
private:
void lockQueue()
{
pthread_mutex_lock(&mutex_);
} // 加锁
void unlockQueue() { pthread_mutex_unlock(&mutex_); } // 解锁
bool haveTask() { return !taskQueue_.empty(); } // 判断有没有任务在执行
void waitForTask() { pthread_cond_wait(&cond_, &mutex_); } // 条件等待
void choiceThreadForHandler() { pthread_cond_signal(&cond_); } // 唤醒等待
T pop()
{
T temp = taskQueue_.front();
taskQueue_.pop();
return temp;
}
private:
bool isStart_;
int threadNum_;
queue<T> taskQueue_; // 任务队列
pthread_mutex_t mutex_; // 锁
pthread_cond_t cond_; // 条件变量
/*条件标志的作用是在一个或多个线程
等待某个条件成立的情况下,阻塞自己,
直到其他线程改变了共享数据并发出
信号告诉等待的线程可以继续执行*/
static ThreadPool<T> *instance; // 设置单例
// const static int a = 100;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
🐡7.3 Task.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include "log.hpp"
class Task
{
public:
//等价于
// typedef std::function<void (int, std::string, uint16_t)> callback_t;
using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
int sock_; // 给用户提供IO服务的sock
uint16_t port_; // client port
std::string ip_; // client ip
callback_t func_; // 回调方法
public:
Task():sock_(-1), port_(-1)
{}
Task(int sock, std::string ip, uint16_t port, callback_t func)
: sock_(sock), ip_(ip), port_(port), func_(func)
{}
void operator () ()
{
logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",\
pthread_self(), ip_.c_str(), port_);
func_(sock_, ip_, port_);
logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",\
pthread_self(), ip_.c_str(), port_);
}
~Task()
{}
};
🐠7.4ClientTcp.cc
cpp
#include "util.hpp"
// 2. 需要bind吗??需要,但是不需要自己显示的bind! 不要自己bind!!!!
// 3. 需要listen吗?不需要的!
// 4. 需要accept吗?不需要的!
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 8081\n"
<< std::endl;
}
// ./clientTcp serverIp serverPort
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. 创建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. connect,向服务器发起链接请求,
// 2.1 先填充需要连接的远端主机的基本信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverPort);
inet_aton(serverIp.c_str(), &server.sin_addr);
// 2.2 发起请求,connect 会自动帮我们进行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 << "请输入你的消息>>> ";
std::getline(std::cin, message); // 结尾不会有\n
if (strcasecmp(message.c_str(), "quit") == 0)
quit = true;
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;
}
🐟 7.5 Lock.hpp
本程序暂不涉及临界资源的访问 互斥锁的机制 可加可不加 我这里加上了 是为了学习和使用。
cpp
#pragma once
#include<iostream>
#include<pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&lock_,nullptr);
}
~Mutex()
{
pthread_mutex_destroy(&lock_);
}
void lock()
{
pthread_mutex_lock(&lock_);
}
void unlock()
{
pthread_mutex_unlock(&lock_);
}
private:
pthread_mutex_t lock_;
};
class LockGuard
{
public:
LockGuard(Mutex *mutex) : mutex_(mutex)
{
mutex_->lock();
std::cout << "加锁成功..." << std::endl;
}
~LockGuard()
{
mutex_->unlock();
std::cout << "解锁成功...." << std::endl;
}
private:
Mutex *mutex_;
};
🐬7.7头文件util.hpp
cpp
#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
🐳7.6Makefile 文件
cpp
.PHONY:all
all:clientTcp serverTcp
clientTcp: ClientTCP.cc
g++ -o $@ $^ -std=c++11
serverTcp:ServerTCP.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f serverTcp clientTcp
🐋7.8 日志文件 log.hpp
cpp
#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_list是一种允许您操作变量参数的类型,
va_start(ap, format); //va_start用于初始化 ava_list以指向第一个变量参数
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);
}
注:本程序是基于Linux系统下进行的,请在Linux环境下进行运行,本文为原创作品,各位转载请说明出处,创作不易,点赞支持技术大涨~~早日进大厂 ,关于程序有任何问题请在评论区留言...
🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕🦺 🐈 🐈⬛