Makefile
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f tcp_client tcp_server
tcp_server.hpp
这段代码是一个简单的TCP服务器实现,可以监听指定的端口,接受客户端的连接请求,并为每个连接提供服务。下面是这段代码的主要实现逻辑:
TcpServer类的构造函数初始化了服务器的端口号和IP地址,initServer()函数用于初始化服务器,包括创建socket、绑定端口和IP地址以及监听连接请求等操作。
start()函数是服务器的主要逻辑,其中使用了accept()函数等待客户端的连接请求,如果有新的连接请求到来,就创建一个子进程为其提供服务。子进程中调用service()函数来处理客户端请求,该函数中使用read()函数读取客户端发送过来的数据,并使用write()函数将其回传给客户端。如果客户端关闭了连接,子进程会退出并成为一个僵尸进程,父进程通过waitpid()函数来回收子进程资源。
service()函数是一个回显服务器,它会将客户端发送过来的数据原封不动地回传给客户端。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
//echo server
//定义了一个大小为1024的字符数组,用于存储从客户端接收到的数据
char buffer[1024];
//用于不断地接收客户端发送过来的数据。
while(true)
{
// read && write 可以直接被使用!
//使用read()函数从sock中读取数据,read()函数的返回值s表示实际读取到的字节数
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
//如果s > 0,说明读取到了数据,将buffer数组中的数据转换为字符串并打印到控制台上,然后通过write()函数将数据回传给客户端。
if(s > 0)
{
buffer[s] = 0; //将发过来的数据当做字符串
std::cout << clientip << ":" << clientport << "# " << buffer << std::endl;
}
//如果s == 0,说明对端关闭了连接,此时打印一条日志信息,并跳出循环。
else if(s == 0) //对端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
//如果s < 0,说明读取数据时发生了错误,此时打印一条错误日志信息,并跳出循环
else{
//
logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
break;
}
write(sock, buffer, strlen(buffer));
}
}
//这段代码实现了一个简单的回显服务器,它可以接收客户端发送过来的数据,并将其原封不动地回传给客户端。
class TcpServer
{
private:
const static int gbacklog = 20; //该变量用于指定在调用 listen() 函数时,内核所维护的未完成连接队列的最大长度。
public:
TcpServer(uint16_t port, std::string ip=""):listensock(-1), _port(port), _ip(ip)
{
}
void initServer() //用于初始化服务器
{
// 1. 创建socket -- 进程和文件
//调用 socket() 函数,指定地址族为 AF_INET(IPv4),传输方式为 SOCK_STREAM(面向连接的 TCP),协议为 0(自动选择协议)。
listensock = socket(AF_INET, SOCK_STREAM, 0);
if(listensock < 0)
{
logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "create socket success, listensock: %d", listensock); // 3
// 2. bind -- 文件 + 网络
//调用 bind() 函数,将套接字和指定的 IP 地址、端口号绑定起来。
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
exit(3);
}
// 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
//调用 listen() 函数,将套接字标记为被动套接字,开始监听连接请求。
if(listen(listensock, gbacklog) < 0)
{
logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "init server success");
}
void start() //用于启动服务器
{
signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
while(true)
{
// sleep(1);
// 4. 获取连接
//sockaddr_in 是一个结构体类型,用于存储 IPv4 地址和端口号信息。
struct sockaddr_in src;
socklen_t len = sizeof(src);
// fd(李四,王五等提供服务的服务员) vs _sock(张三 --- 获取新连接)
//调用 accept() 函数,等待客户端连接请求。如果连接请求成功,将返回一个新的套接字(servicesock),可以用于与客户端进行通信。
int servicesock = accept(listensock, (struct sockaddr*)&src, &len);
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
// 获取连接成功了
//将 sockaddr_in 结构体变量 src 中的 sin_port 字段从网络字节序转换为本地字节序,以获取客户端连接请求中的端口号。
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",\
servicesock, client_ip.c_str(), client_port);
// 开始进行通信服务啦
// version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
// 很显然,是不能够直接被使用的! -- 为什么? 单进程
// service(servicesock, client_ip, client_port);
// version 2.0 -- 多进程版 --- 创建子进程
// 让子进程给新的连接提供服务,子进程能不能打开父进程曾经打开的文件fd呢?1 0
//创建一个子进程,用于处理新的连接请求。在子进程中,调用 service() 函数,用于提供服务。在服务完成后,调用 exit() 函数结束子进程。
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 子进程, 子进程会不会继承父进程打开的文件与文件fd呢?1, 0
// 子进程是来进行提供服务的,需不需要知道监听socket呢?
close(listensock);
service(servicesock, client_ip, client_port);
exit(0); // 僵尸状态
}
close(servicesock); // 如果父进程关闭servicesock,会不会影响子进程??下节课
// 父进程
// waitpid(); //
}
}
~TcpServer(){
}
private:
uint16_t _port;
std::string _ip;
int listensock;
};
tcp_server.cc
这段代码是一个 TCP 服务器的实现。
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
//usage(argv[0]);:如果参数个数不为 2,调用 usage 函数打印程序的使用方法。
usage(argv[0]);
exit(1);
}
//如果参数个数为 2,将第二个参数转换为整数类型并赋值给 port 变量。
uint16_t port = atoi(argv[1]);
//创建一个 TcpServer 对象,将监听的端口号作为参数传入构造函数。
std::unique_ptr<TcpServer> svr(new TcpServer(port));
//初始化 TcpServer 对象。
svr->initServer();
//启动 TcpServer 对象。
svr->start();
return 0;
}
tcp_client.cc (客户端)
#include <iostream>
int main()
{
return 0;
}
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
// FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}