listen()
c
功能:listen - listen for connections on a socket
// 头文件
#include <sys/types.h>
#include <sys/socket.h>
// 函数
int listen(int sockfd, int backlog);
// 返回值
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
accept()
c
功能:accept, accept4 - accept a connection on a socket
// 头文件
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// 函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
// 参数
struct sockaddr *addr:输出型参数,可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
// 返回值
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is setappropriately.
// 如果成功,这些系统调用将返回一个非负整数,该整数是所接收套接字的文件描述符。如果出现错误,则返回-1,并适当地设置errno。
connect()
c
功能: connect - initiate a connection on a socket
// 头文件
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
// 函数
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
// 参数
int sockfd:用户端套接字的文件描述符
const struct sockaddr *addr: 输出型参数,结构体内部包含了指定服务器的IP地址和端口号
socklen_t addrlen:是const struct sockaddr *addr的长度
// 功能:
The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument spec‐ifies the size of addr. The format of the address in addr is determined by the address space of the socket sockfd; see socket(2) for further details.
// connect()系统调用将文件描述符sockfd引用的套接字连接到由addr指定的地址。addrlen参数指定addr的大小。addr中的地址格式由套接字sockfd的地址空间决定;详细信息请参见socket(2)
// 返回值
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately.
关于网络命令行命令
c
netstat -nltp // l表示listen监听, t,代表tcp
tcp演示代码1(基础框架)
makefile
c
cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpclient:tcpClient.cc
$(cc) -o $@ $^ -std=c++11
tcpserver:tcpServer.cc
$(cc) -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpserver tcpclient
log.hpp
cc
#pragma once
#include <iostream>
#include <string>
// 代表错误等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
void logMessage(int level, const std::string &message)
{
// [日志等级] [时间戳/时间] [pid] [messge]
// 例如:[WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]
std::cout << message << std::endl;
}
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
using namespace std;
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "bind socket success");
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success");
cout << "sock " << sock << endl;
// 5. 这里就是一个sock,未来通信我们就用这个sock,面向字节流的
// sock就是一个文件描述符,因此后续全部都是文件操作
serviceIO(sock);
// 对一个已经使用完毕的sock,我们要关闭这个sock,要不然会导致,文件描述符泄漏
close(sock);
}
}
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串
// 将字符串的结尾添加"\0"
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size());
}
else if(n == 0)
{
// n == 0, 代表读到了文件结尾
// 就是客户端作为写端,将写端关闭了,那么读端就会读到文件文件结尾, 那就为0
// 代表client(客户端)退出了
// 因为客户端已经退出了,那么服务器也就不需要再进行什么操作了
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
tcpServer.cc
c
#include "tcpServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
tsvr->start();
return 0;
}
tcpClient.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
// 客户端的构造函数
TcpClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{}
// 初始化客户端
void initClient()
{
// 1. 创建套接字socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
// 创建套接字失败,打印错误信息
std::cerr << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind?要的!
// 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
// 3. 要不要listen?不要!
// 4. 要不要accept? 不要!
// 5. 要什么呢? 要发起链接!
}
// 启动客户端
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
// 客户端向服务器发起链接失败,则打印错误信息
std::cerr << "socket connect error" << std::endl;
}
else
{
// 链接成功,客户端和服务端进行通信
std::string msg;
while(true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer)-1);
if(n > 0)
{
//目前我们把读到的数据当成字符串, 截止目前
buffer[n] = 0;
std::cout << "Server回显# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
// 客户端的析构函数
~TcpClient()
{
// 关闭客户端套接字对应的文件描述符
if(_sock >= 0) close(_sock);
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
tcpClient.cc
c
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
tcp演示代码2(多进程版)
版本1(孙子进程法)
- 只需要修改tcpServer.hpp就可以,其他文件不变
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
using namespace std;
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "bind socket success");
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success");
cout << "sock " << sock << endl;
// version2 (多进程版)
// 子进程会继承父进程的文件描述符等,因此子进程也可以看到sock,和_listensock等
pid_t id = fork();
if (id == 0) // child
{
// 创建子进程来向外部的用户提供服务
// 因为子进程只负责和外部的客户端进程通信,而不需要监听客户端的请求
// 所以,在子进程中,我们需要关闭负责监听的套接字对应的文件描述符_listensock
close(_listensock);
if(fork()>0)
{
// 由于爷爷进程在等待回收父亲进程
// 当fork()>0,说明此时的进程是父亲进程
// 直接退出父亲进程,那么爷爷进程就可以立马回收父亲进程
exit(0);
}
// 此时为孙子进程在提供通讯服务
serviceIO(sock);
close(sock);
// 因为父亲进程已经退出了,所以当孙子进程退出后
// 孙子进程变为孤儿进程,会被OS领养,由OS来回收孙子进程
exit(0);
}
// 此处爷爷进程必须关闭sock文件描述符,就是accept()返回的文件描述符,也就是和客户端提供通讯的文件描述符
// 如果没有关闭,老用户退出,老用户占用的文件描述符,依旧被占用
// 当接收新用户,则accept()会返回文件描述符,已经被占用的无法被利用,则文件描述符迟早会泄漏完,则进程崩溃
close(sock);
//father(爷爷进程)
// 等待回收父亲进程
pid_t ret = waitpid(id, nullptr, 0);
if(ret>0)
{
std::cout << "waitsuccess: " << ret << std::endl;
}
}
}
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串
// 将字符串的结尾添加"\0"
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size());
}
else if(n == 0)
{
// n == 0, 代表读到了文件结尾
// 就是客户端作为写端,将写端关闭了,那么读端就会读到文件文件结尾, 那就为0
// 代表client(客户端)退出了
// 因为客户端已经退出了,那么服务器也就不需要再进行什么操作了
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
演示结果

版本2(信号忽略法)
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
using namespace std;
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "bind socket success");
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
// 开始启动服务器
void start()
{
// 参数1 int signum: 就是信号的种类,这里是通过信号回收子进程的SIGCHLD. 子进程要终止了,发个SIGCHLD信号告诉父进程
// 参数2 sighandler_t handler: 这里选的是SIG_IGN, 忽视信号
// 这个函数总的意思就是: 我父进程不关心你子进程啥时候要退出,
// 一会你退了以后内核把pcb那些该回收的那些回收了就行了(包括子进程号、运行时间信息,占用的内存,mask等等),别告诉我了嚎
signal(SIGCHLD, SIG_IGN);
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success");
cout << "sock " << sock << endl;
// version2 (多进程版)
// 子进程会继承父进程的文件描述符等,因此子进程也可以看到sock,和_listensock等
pid_t id = fork();
if (id == 0) // child
{
// 创建子进程来向外部的用户提供服务
// 因为子进程只负责和外部的客户端进程通信,而不需要监听客户端的请求
// 所以,在子进程中,我们需要关闭负责监听的套接字对应的文件描述符_listensock
close(_listensock);
serviceIO(sock);
close(sock);
exit(0);
// 子进程退出,子进程会自动发送信号给父亲程,并由内核将其资源进行销毁
}
// 此处爷爷进程必须关闭sock文件描述符,就是accept()返回的文件描述符,也就是和客户端提供通讯的文件描述符
// 如果没有关闭,老用户退出,老用户占用的文件描述符,依旧被占用
// 当接收新用户,则accept()会返回文件描述符,已经被占用的无法被利用,则文件描述符迟早会泄漏完,则进程崩溃
close(sock);
}
}
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串
// 将字符串的结尾添加"\0"
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size());
}
else if (n == 0)
{
// n == 0, 代表读到了文件结尾
// 就是客户端作为写端,将写端关闭了,那么读端就会读到文件文件结尾, 那就为0
// 代表client(客户端)退出了
// 因为客户端已经退出了,那么服务器也就不需要再进行什么操作了
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
演示结果

tcp演示代码3(多线程版)
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
using namespace std;
#include "log.hpp"
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// 声明 class TcpServer已经存在
// 由于在class ThreadData使用了class TcpServer,但是此时还没有生成class TcpServer
// 因此要进行事先声明
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
{
}
public:
TcpServer *_self;
int _sock;
};
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "bind socket success");
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
// 开始启动服务器
void start()
{
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success");
cout << "sock " << sock << endl;
// version3 多线程版
pthread_t tid;
// 由于需要传递this指针和文件描述符sock
// 因此将其封装为一个类,创建类对象,就可以一起进行传递了
ThreadData *td = new ThreadData(this, sock);
pthread_create(&tid, nullptr, threadRoutine, td);
// 由于在threadRoutine内部已经进行了线程分离,因此主线程不需要再等待回收子线程了
// pthread_join(tid, nullptr);
}
}
static void *threadRoutine(void *args)
{
// 分离这个子线程,那么主线程就不需要专门去回收这个子线程了
// 指定该状态,线程主动与主控线程断开关系。
// 线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
// 子线程与客户端进行通信服务
td->_self->serviceIO(td->_sock);
// 通信完成后,关闭通信的文件描述符
close(td->_sock);
// 并释放作为参数的对象td
delete td;
return nullptr;
}
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串
// 将字符串的结尾添加"\0"
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += "server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size());
}
else if (n == 0)
{
// n == 0, 代表读到了文件结尾
// 就是客户端作为写端,将写端关闭了,那么读端就会读到文件文件结尾, 那就为0
// 代表client(客户端)退出了
// 因为客户端已经退出了,那么服务器也就不需要再进行什么操作了
logMessage(NORMAL, "client quit, me too!");
break;
}
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
演示结果

tcp演示代码4(线程池版)
makefile
c
cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpclient:tcpClient.cc
$(cc) -o $@ $^ -std=c++11
tcpserver:tcpServer.cc
$(cc) -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpserver tcpclient
log.hpp
c
#pragma once
#include <iostream>
#include <string>
// 代表错误等级
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
void logMessage(int level, const std::string &message)
{
// [日志等级] [时间戳/时间] [pid] [messge]
// 例如:[WARNING] [2023-05-11 18:09:08] [123] [创建socket失败]
std::cout << message << std::endl;
}
LockGuard.hpp
c
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
// 构造函数
Mutex(pthread_mutex_t *lock_p = nullptr)
:lock_p_(lock_p)
{}
// 加锁
void lock()
{
// 如果lock_p_不是空指针,这说明已经传递进来了一把锁了
// 此时对调用的线程进行加锁
if(lock_p_)
pthread_mutex_lock(lock_p_); // 进行加锁
}
// 解锁
void unlock()
{
if(lock_p_)
pthread_mutex_unlock(lock_p_); // 进行解锁
}
// 析构函数
~Mutex()
{}
private:
pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex)
:mutex_(mutex)
{
mutex_.lock(); // 在构造函数中进行加锁
}
// 我们自己封装的锁,一旦出了其作用域,系统就会调用析构函数对调用的线程进行解锁
~LockGuard()
{
mutex_.unlock(); // 在析构函数中进行解锁
}
private:
Mutex mutex_;
};
Task.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <functional>
#include "log.hpp"
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串, 截止目前
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += " server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size()); // 多路转接
}
else if (n == 0)
{
// 代表client退出
logMessage(NORMAL, "client quit, me too!");
break;
}
}
close(sock);
}
class Task
{
using func_t = std::function<void(int)>;
public:
// 无参的构造函数
Task()
{}
// sock就是通信的套接字文件描述符
// 回调函数需要执行的回调函数,也就是负责服务器与用户通信的功能
Task(int sock, func_t func)
: _sock(sock), _callback(func)
{}
// 使用仿函数来运行这个回调函数,也就是开始执行通信任务
void operator()()
{
_callback(_sock);
}
private:
int _sock;
func_t _callback;
};
Thread.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
namespace ThreadNs
{
typedef std::function<void *(void *)> func_t;
const int num = 1024;
class Thread
{
private:
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
// 类内成员,有缺省参数; 不仅需要传递当前函数的地址,还要传递this指针
// 而我们只能够传递一个参数(当前函数的地址),因此必须将当前成员函数设置为静态
static void *start_routine(void *args)
{
Thread *_this = static_cast<Thread *>(args);
return _this->callback();
}
public:
// 构造函数
Thread()
{
// 给创建的线程命名
char namebuffer[num];
snprintf(namebuffer, sizeof namebuffer, "thread-%d", threadnum++);
// name_就是新创建线程的名称
name_ = namebuffer;
}
// 创建线程
void start(func_t func, void *args = nullptr)
{
func_ = func; // 线程要调用的方法
args_ = args; // args_是func_的参数
// 使用系统接口来创建线程
// 由于start_routine是类内成员函数,因此其必须为静态成员函数
int n = pthread_create(&tid_, nullptr, start_routine, this);
assert(n == 0);
(void)n;
}
// 等待回收进程
void join()
{
int n = pthread_join(tid_, nullptr);
assert(n == 0);
(void)n;
}
// 取调用线程的线程名
std::string threadname()
{
return name_;
}
// 析构函数
~Thread()
{
// do nothing
}
// 执行线程调用函数start_routine()的计算任务
void *callback()
{
return func_(args_);
}
private:
std::string name_; // 线程名
func_t func_; // 线程要执行的方法
void *args_; // func_的参数
pthread_t tid_; // 线程id
static int threadnum; // 线程名的后缀序号
};
int Thread::threadnum = 1; // 对静态变量,在全局处进行初始化
} // end namespace ThreadNs
ThreadPool.hpp
c
#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>
using namespace ThreadNs;
// 线程池中,线程的默认数量(如果我们不进行传参的话)
const int gnum = 10;
// 在ThreadData用到了ThreadPool,因此需要对其先进行声明
template <class T>
class ThreadPool;
// 将ThreadPool对象和线程名封装为一个类,
// 将这个类对象作为参数就可以传递两个参数
template <class T>
class ThreadData
{
public:
ThreadPool<T> *threadpool; // ThreadPool的类指针
std::string name; // 调用线程的线程名
public:
// 构造函数
ThreadData(ThreadPool<T> *tp, const std::string &n)
: threadpool(tp)
, name(n)
{}
};
template <class T>
class ThreadPool
{
private:
// 当线程池中的所有线程都运行起来时,都会执行handlerTask
// 而handlerTask的作用就是检测任务队列中是否存在消费任务
// 有,则对消费任务进行计算
// 没有,则消费线程在对应的条件变量下进行等待
// handlerTask是静态成员函数,因此传递这个成员函数不需要传递this指针的
static void *handlerTask(void *args)
{
// 参数args是ThreadPool的类对象指针this
ThreadData<T> *td = (ThreadData<T> *)args;
while (true)
{
T t;
{
// 多个消费线程访问任务队列时是互斥的,因此需要进行加锁
// 使用的锁是我们自己进行封装的锁
LockGuard lockguard(td->threadpool->mutex());
while (td->threadpool->isQueueEmpty())
{
// 如果任务队列为空,则线程在对应的条件变量下进行等待
td->threadpool->threadWait();
}
// pop的本质,是将任务从公共队列中,拿到当前线程自己独立的栈中
t = td->threadpool->pop();
}
// 开始执行任务
t();
}
// 释放ThreadPool的类对象指针this
delete td;
return nullptr;
}
// 线程池的构造函数
ThreadPool(const int &num = gnum)
: _num(num)
{
// 对锁和条件变量进行初始化
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
// 创建新线程,并将新线程放入到vector容器当中
for (int i = 0; i < _num; i++)
{
_threads.push_back(new Thread());
}
}
// 单例模式,只允许有一个对象来对这个类的数据进行管理
// 因此,需要防止赋值拷贝和拷贝构造
void operator=(const ThreadPool &) = delete;
ThreadPool(const ThreadPool &) = delete;
public:
// 判断任务队列是否为空
bool isQueueEmpty() { return _task_queue.empty(); }
// 让线程在对应的条件变量下进行等待
void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
// 从任务队列中拿出消费任务
T pop()
{
// 拿出消费任务
T t = _task_queue.front();
// 从任务队列中将其弹出
_task_queue.pop();
return t;
}
// 将私有成员变量锁,进行封装成为成员函数
// 这样,才可以在类的外部通过调用成员函数,进而调用私有成员变量
pthread_mutex_t *mutex()
{
return &_mutex;
}
// 启动运行这个线程
void run()
{
// 通过for循环启动所有的线程
for (const auto &t : _threads)
{
// 将ThreadPool的this指针和对应线程的线程名封装为一个类,创建这个类对象
// 这样就可以同时传递两个参数
ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
// 启动线程来处理任务
t->start(handlerTask, td);
std::cout << t->threadname() << " start ..." << std::endl;
}
}
// 生产线程向任务队列放置任务
void push(const T &in)
{
// 多个生产线程在进入任务队列时,他们之间是互斥的,因此需要进行加锁
// LockGuard是我们自己封装的锁对象,当其作用域被销毁,其会调用析构函数释放锁
LockGuard lockguard(&_mutex);
// 将生产的任务放入到任务队列之中
_task_queue.push(in);
// 当任务队列中有任务之后,我们就可以唤醒消费线程来进行消费了
pthread_cond_signal(&_cond);
}
~ThreadPool()
{
// 销毁锁和条件变量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
// 将vector容器中的线程对象所占据的空间进行释放
// 我们自己封装的线程,因此每一个线程对象都需要进行释放
for (const auto &t : _threads)
delete t;
}
// getInstance() 获取实例
// 此处getInstance()需要设置为静态成员函数
// 如果是非静态成员函数,那么获取的实例即属于这个类又属于对象
// 我们想要达到的效果是一个实例只属于类(即一个类一个实例)
// 静态成员函数,不需要创建类对象,指定类域就可以直接进行调用
// 静态成员函数,在类内进行传递,是不需要传递this指针的(因为静态成员函数是没有this指针的)
static ThreadPool<T> *getInstance()
{
if (nullptr == tp)
{
_singlock.lock();
if (nullptr == tp)
{
tp = new ThreadPool<T>();
}
_singlock.unlock();
}
return tp;
}
private:
// 线程池中线程的个数
int _num;
// 将创建好的线程放入vector中,vector就是线程池的容器
std::vector<Thread *> _threads;
// 生产线程将生产的任务放入到任务队列中,消费线程从任务队列中拿任务
std::queue<T> _task_queue;
// 定义一把锁,作为我们封装的锁对象的参数
pthread_mutex_t _mutex;
// 条件变量
pthread_cond_t _cond;
// 线程池的单例,需要设置为静态成员变量,这样这个单例才只属于这个类,和类对象无关
static ThreadPool<T> *tp;
// 创建单例时,可能有多个线程都要创建,因此需要对其进行加锁
static std::mutex _singlock;
};
// 对静态的成员变量做初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
// 对静态的成员变量做初始化
template <class T>
std::mutex ThreadPool<T>::_singlock;
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// 声明 class TcpServer已经存在
// 由于在class ThreadData使用了class TcpServer,但是此时还没有生成class TcpServer
// 因此要进行事先声明
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
{
}
public:
TcpServer *_self;
int _sock;
};
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{
}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "bind socket success");
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "listen socket success");
}
// 开始启动服务器
void start()
{
// 线程池初始化
// 线程池run()以后,所有的线程就会被启动,且一旦检测到任务队列中存在任务
// 就会对任务进行处理
ThreadPool<Task>::getInstance()->run();
logMessage(NORMAL, "Thread init success");
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success");
cout << "sock " << sock << endl;
// version4 线程池版
// 将构建的任务push到线程池的任务队列中
// Task(sock, serviceIO) 就是构建的任务,是一个匿名对象
ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
tcpServer.cc
c
#include "tcpServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
tsvr->start();
return 0;
}
tcpClient.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
// 客户端的构造函数
TcpClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{}
// 初始化客户端
void initClient()
{
// 1. 创建套接字socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
// 创建套接字失败,打印错误信息
std::cerr << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind?要的!
// 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
// 3. 要不要listen?不要!
// 4. 要不要accept? 不要!
// 5. 要什么呢? 要发起链接!
}
// 启动客户端
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
// 客户端向服务器发起链接失败,则打印错误信息
std::cerr << "socket connect error" << std::endl;
}
else
{
// 链接成功,客户端和服务端进行通信
std::string msg;
while(true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer)-1);
if(n > 0)
{
//目前我们把读到的数据当成字符串, 截止目前
buffer[n] = 0;
std::cout << "Server回显# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
// 客户端的析构函数
~TcpClient()
{
// 关闭客户端套接字对应的文件描述符
if(_sock >= 0) close(_sock);
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
tcpClient.cc
c
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
演示结果

tcp演示代码5(完善日志等级)
va_list模拟实现printf
va_list的原理
可变参数列表
c
// 以下是一段伪代码
void logMessage(DEBUG, "hello %f, %d, %c", 3.14, 10, 'C');
// 参数3(...):代表至少会传递0个以上的参数
void logMessage(int level, const char *format, ...)
{
// va_list 其实就是char*类型
// start就是一个指针,start指向的就是这三个参数的起始地址(就是这三个参数3.14, 10, 'C')
va_list start;
// va_start()就是保证start指向第一个参数(3,14)的其实地址
// logMessage(int level, const char *format, ...); 在栈帧中,logMessage的三个参数是从最右边的参数开始先入栈的,所以离可变参数(可变参数的第一个参数)最近的参数是format
// va_start(start,format)就是根据format的地址来定位,使start指向format
va_start(start,format);
//va_arg(start, float) 就是让start向后移动float个字节的地址,这样start就可以指向可变参数的第一个参数(3.14)
// 假设*p指向的是hello
while (*p)
{
switch (*p)
{
case '%':
p++;
if (*p == 'f')
// 此时start指向的就是3.14,并将3.14返回给arg
arg = va_arg(start, float);
...
}
}
// 将start指针变为nullptr
va_end(start);
}
time
c
NAME
time - get time
SYNOPSIS
#include <time.h>
time_t time(time_t *tloc);
DESCRIPTION
The time() function shall return the value of time in seconds since the Epoch.
// time()函数将返回从Epoch开始以秒为单位的时间值。 Epoch n.时代,纪元
The tloc argument points to an area where the return value is also stored. If tloc is a null pointer, no value is stored.
// tloc参数指向也存储返回值的区域。如果tloc是空指针,则不存储任何值。
RETURN VALUE
Upon successful completion, time() shall return the value of time. Otherwise, (time_t)-1 shall be returned.
// Upon successful completion, time() shall return the value of time. Otherwise, (time_t)-1 shall be returned.
gettimeofday
c
NAME
gettimeofday, settimeofday - get / set time
SYNOPSIS
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv, const struct timezone *tz);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
settimeofday(): _BSD_SOURCE
DESCRIPTION
The functions gettimeofday() and settimeofday() can get and set the time as well as a timezone. The tv argument is a struct timeval (as specified in
<sys/time.h>):
// 函数gettimeofday()和settimeofday()可以获取和设置时间以及时区。
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
and gives the number of seconds and microseconds since the Epoch (see time(2)). The tz argument is a struct timezone:
// 并给出从Epoch开始的秒数和微秒数(参见time(2))。参数tz是一个timezone结构体:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
If either tv or tz is NULL, the corresponding structure is not set or returned. (However, compilation warnings will result if tv is NULL.)
// 如果tv或tz为NULL,则不设置或返回相应的结构。(但是,如果tv为NULL,则会产生编译警告。)
The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL. (See NOTES below.)
// 使用时区结构已经过时;tz参数通常应指定为NULL。(见下文注释)
RETURN VALUE
gettimeofday() and settimeofday() return 0 for success, or -1 for failure (in which case errno is set appropriately).
// Gettimeofday()和settimeofday()成功返回0,失败返回-1(在这种情况下errno被适当设置)。
localtime
c
NAME
asctime, ctime, gmtime, localtime, mktime, asctime_r, ctime_r, gmtime_r, localtime_r - transform date and time to broken-down time or ASCII
// 将日期和时间转换为分解时间或ASCII
SYNOPSIS
#include <time.h>
struct tm *localtime(const time_t *timep);
// const time_t *timep: 就是时间戳
DESCRIPTION
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
RETURN VALUE
Each of these functions returns the value described, or NULL (-1 in case of mktime()) in case an error was detected.
// 这些函数中的每一个都返回描述的值,如果检测到错误,则返回NULL (mktime()的情况下为-1)。
log.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// 错误等级
// 如果错误等级只是"WARNING",服务器还可以继续运行
// 如果错误等级是"FATAL",那么服务器就会直接挂掉
const char * to_levelstr(int level)
{
switch(level)
{
case DEBUG : return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
default : return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
char logprefix[NUM];
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
va_start(arg, format);
vsnprintf(logcontent, sizeof(logcontent), format, arg);
std::cout << logprefix << logcontent << std::endl;
}
makefile
c
cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpclient:tcpClient.cc
$(cc) -o $@ $^ -std=c++11
tcpserver:tcpServer.cc
$(cc) -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpserver tcpclient
LockGuard.hpp
c
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
// 构造函数
Mutex(pthread_mutex_t *lock_p = nullptr)
:lock_p_(lock_p)
{}
// 加锁
void lock()
{
// 如果lock_p_不是空指针,这说明已经传递进来了一把锁了
// 此时对调用的线程进行加锁
if(lock_p_)
pthread_mutex_lock(lock_p_); // 进行加锁
}
// 解锁
void unlock()
{
if(lock_p_)
pthread_mutex_unlock(lock_p_); // 进行解锁
}
// 析构函数
~Mutex()
{}
private:
pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex)
:mutex_(mutex)
{
mutex_.lock(); // 在构造函数中进行加锁
}
// 我们自己封装的锁,一旦出了其作用域,系统就会调用析构函数对调用的线程进行解锁
~LockGuard()
{
mutex_.unlock(); // 在析构函数中进行解锁
}
private:
Mutex mutex_;
};
Task.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <functional>
#include "log.hpp"
void serviceIO(int sock)
{
char buffer[1024];
while (true)
{
ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
if (n > 0)
{
// 目前我们把读到的数据当成字符串, 截止目前
buffer[n] = 0;
std::cout << "recv message: " << buffer << std::endl;
std::string outbuffer = buffer;
outbuffer += " server[echo]";
write(sock, outbuffer.c_str(), outbuffer.size()); // 多路转接
}
else if (n == 0)
{
// 代表client退出
logMessage(NORMAL, "client quit, me too!");
break;
}
}
close(sock);
}
class Task
{
using func_t = std::function<void(int)>;
public:
// 无参的构造函数
Task()
{}
// sock就是通信的套接字文件描述符
// 回调函数需要执行的回调函数,也就是负责服务器与用户通信的功能
Task(int sock, func_t func)
: _sock(sock), _callback(func)
{}
// 使用仿函数来运行这个回调函数,也就是开始执行通信任务
void operator()()
{
_callback(_sock);
}
private:
int _sock;
func_t _callback;
};
Thread.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
namespace ThreadNs
{
typedef std::function<void *(void *)> func_t;
const int num = 1024;
class Thread
{
private:
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
// 类内成员,有缺省参数; 不仅需要传递当前函数的地址,还要传递this指针
// 而我们只能够传递一个参数(当前函数的地址),因此必须将当前成员函数设置为静态
static void *start_routine(void *args)
{
Thread *_this = static_cast<Thread *>(args);
return _this->callback();
}
public:
// 构造函数
Thread()
{
// 给创建的线程命名
char namebuffer[num];
snprintf(namebuffer, sizeof namebuffer, "thread-%d", threadnum++);
// name_就是新创建线程的名称
name_ = namebuffer;
}
// 创建线程
void start(func_t func, void *args = nullptr)
{
func_ = func; // 线程要调用的方法
args_ = args; // args_是func_的参数
// 使用系统接口来创建线程
// 由于start_routine是类内成员函数,因此其必须为静态成员函数
int n = pthread_create(&tid_, nullptr, start_routine, this);
assert(n == 0);
(void)n;
}
// 等待回收进程
void join()
{
int n = pthread_join(tid_, nullptr);
assert(n == 0);
(void)n;
}
// 取调用线程的线程名
std::string threadname()
{
return name_;
}
// 析构函数
~Thread()
{
// do nothing
}
// 执行线程调用函数start_routine()的计算任务
void *callback()
{
return func_(args_);
}
private:
std::string name_; // 线程名
func_t func_; // 线程要执行的方法
void *args_; // func_的参数
pthread_t tid_; // 线程id
static int threadnum; // 线程名的后缀序号
};
int Thread::threadnum = 1; // 对静态变量,在全局处进行初始化
} // end namespace ThreadNs
ThreadPool.hpp
c
#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <mutex>
#include <pthread.h>
#include <unistd.h>
using namespace ThreadNs;
// 线程池中,线程的默认数量(如果我们不进行传参的话)
const int gnum = 10;
// 在ThreadData用到了ThreadPool,因此需要对其先进行声明
template <class T>
class ThreadPool;
// 将ThreadPool对象和线程名封装为一个类,
// 将这个类对象作为参数就可以传递两个参数
template <class T>
class ThreadData
{
public:
ThreadPool<T> *threadpool; // ThreadPool的类指针
std::string name; // 调用线程的线程名
public:
// 构造函数
ThreadData(ThreadPool<T> *tp, const std::string &n)
: threadpool(tp)
, name(n)
{}
};
template <class T>
class ThreadPool
{
private:
// 当线程池中的所有线程都运行起来时,都会执行handlerTask
// 而handlerTask的作用就是检测任务队列中是否存在消费任务
// 有,则对消费任务进行计算
// 没有,则消费线程在对应的条件变量下进行等待
// handlerTask是静态成员函数,因此传递这个成员函数不需要传递this指针的
static void *handlerTask(void *args)
{
// 参数args是ThreadPool的类对象指针this
ThreadData<T> *td = (ThreadData<T> *)args;
while (true)
{
T t;
{
// 多个消费线程访问任务队列时是互斥的,因此需要进行加锁
// 使用的锁是我们自己进行封装的锁
LockGuard lockguard(td->threadpool->mutex());
while (td->threadpool->isQueueEmpty())
{
// 如果任务队列为空,则线程在对应的条件变量下进行等待
td->threadpool->threadWait();
}
// pop的本质,是将任务从公共队列中,拿到当前线程自己独立的栈中
t = td->threadpool->pop();
}
// 开始执行任务
t();
}
// 释放ThreadPool的类对象指针this
delete td;
return nullptr;
}
// 线程池的构造函数
ThreadPool(const int &num = gnum)
: _num(num)
{
// 对锁和条件变量进行初始化
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
// 创建新线程,并将新线程放入到vector容器当中
for (int i = 0; i < _num; i++)
{
_threads.push_back(new Thread());
}
}
// 单例模式,只允许有一个对象来对这个类的数据进行管理
// 因此,需要防止赋值拷贝和拷贝构造
void operator=(const ThreadPool &) = delete;
ThreadPool(const ThreadPool &) = delete;
public:
// 判断任务队列是否为空
bool isQueueEmpty() { return _task_queue.empty(); }
// 让线程在对应的条件变量下进行等待
void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
// 从任务队列中拿出消费任务
T pop()
{
// 拿出消费任务
T t = _task_queue.front();
// 从任务队列中将其弹出
_task_queue.pop();
return t;
}
// 将私有成员变量锁,进行封装成为成员函数
// 这样,才可以在类的外部通过调用成员函数,进而调用私有成员变量
pthread_mutex_t *mutex()
{
return &_mutex;
}
// 启动运行这个线程
void run()
{
// 通过for循环启动所有的线程
for (const auto &t : _threads)
{
// 将ThreadPool的this指针和对应线程的线程名封装为一个类,创建这个类对象
// 这样就可以同时传递两个参数
ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
// 启动线程来处理任务
t->start(handlerTask, td);
// std::cout << t->threadname() << " start ..." << std::endl;
// 使用日志来进行打印(DEBUG代表调试)
logMessage(DEBUG, "%s start ...", t->threadname().c_str());
}
}
// 生产线程向任务队列放置任务
void push(const T &in)
{
// 多个生产线程在进入任务队列时,他们之间是互斥的,因此需要进行加锁
// LockGuard是我们自己封装的锁对象,当其作用域被销毁,其会调用析构函数释放锁
LockGuard lockguard(&_mutex);
// 将生产的任务放入到任务队列之中
_task_queue.push(in);
// 当任务队列中有任务之后,我们就可以唤醒消费线程来进行消费了
pthread_cond_signal(&_cond);
}
~ThreadPool()
{
// 销毁锁和条件变量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
// 将vector容器中的线程对象所占据的空间进行释放
// 我们自己封装的线程,因此每一个线程对象都需要进行释放
for (const auto &t : _threads)
delete t;
}
// getInstance() 获取实例
// 此处getInstance()需要设置为静态成员函数
// 如果是非静态成员函数,那么获取的实例即属于这个类又属于对象
// 我们想要达到的效果是一个实例只属于类(即一个类一个实例)
// 静态成员函数,不需要创建类对象,指定类域就可以直接进行调用
// 静态成员函数,在类内进行传递,是不需要传递this指针的(因为静态成员函数是没有this指针的)
static ThreadPool<T> *getInstance()
{
if (nullptr == tp)
{
_singlock.lock();
if (nullptr == tp)
{
tp = new ThreadPool<T>();
}
_singlock.unlock();
}
return tp;
}
private:
// 线程池中线程的个数
int _num;
// 将创建好的线程放入vector中,vector就是线程池的容器
std::vector<Thread *> _threads;
// 生产线程将生产的任务放入到任务队列中,消费线程从任务队列中拿任务
std::queue<T> _task_queue;
// 定义一把锁,作为我们封装的锁对象的参数
pthread_mutex_t _mutex;
// 条件变量
pthread_cond_t _cond;
// 线程池的单例,需要设置为静态成员变量,这样这个单例才只属于这个类,和类对象无关
static ThreadPool<T> *tp;
// 创建单例时,可能有多个线程都要创建,因此需要对其进行加锁
static std::mutex _singlock;
};
// 对静态的成员变量做初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
// 对静态的成员变量做初始化
template <class T>
std::mutex ThreadPool<T>::_singlock;
tcpServer.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
using namespace std;
namespace server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
// 默认缺省的端口号为8080
static const uint16_t gport = 8080;
static const int gbacklog = 5;
// 声明 class TcpServer已经存在
// 由于在class ThreadData使用了class TcpServer,但是此时还没有生成class TcpServer
// 因此要进行事先声明
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *self, int sock) : _self(self), _sock(sock)
{}
public:
TcpServer *_self;
int _sock;
};
class TcpServer
{
public:
// tcp服务器的构造函数
TcpServer(const uint16_t &port = gport) : _listensock(-1), _port(port)
{}
// 初始化服务器
void initServer()
{
// 1. 创建socket文件套接字对象
// tcp也是网络连接,因此第一个参数为AF_INET
// tcp是面向字节流的,因此第二个参数为SOCK_STREAM
// 确定好前两个参数,第三个参数默认为0就可以
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
// 套接字的文件描述符小于0,那么创建套接字失败
// 则我们通过日志,打印对应的错误信息
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
// 2. bind绑定自己的网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// bind小于0,代表绑定失败
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
// 通过日志,打印绑定失败的错误信息
logMessage(FATAL, "bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL, "bind socket success");
// tcp是建立连接的协议,因此客户端和服务器是要建立连接的
// 所谓的连接,也就是服务器实时知道客户端向其发出了请求
// tcp协议,客户端向服务器发送消息,需要先和服务器建立连接
// 然后才可以向服务器发送消息
// 为了实时知道客户端发出的请求,因此需要设置socket为监听状态
// 监听状态:如果你是一名接线员,你如果处于监听状态,你就需要实时坐在电话旁边
// 等待客户的电话,这样才可以实时与客户建立连接
// 3. 设置socket 为监听状态
// 参数_listensock,就是要将这个参数设置为监听状态
// gbacklog,后续再说其用处,目前暂时先设置为5
if (listen(_listensock, gbacklog) < 0) // 第二个参数backlog后面在填这个坑
{
// // 通过日志,打印设置监听状态失败的错误信息
logMessage(FATAL, "listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL, "create socket success: %d", _listensock);
}
// 开始启动服务器
void start()
{
// 线程池初始化
// 线程池run()以后,所有的线程就会被启动,且一旦检测到任务队列中存在任务
// 就会对任务进行处理
ThreadPool<Task>::getInstance()->run();
logMessage(NORMAL, "Thread init success");
// 4. server 获取新链接
// _listensock监听到新的用户发出的请求,则分配一个新的套接字sock与用户进行通信
// _listensock只负责监听用户请求,新的套接字sock只负责通信
// sock, 和client进行通信的fd(文件描述符)
for (;;)
{
// 参数
// struct sockaddr *addr : 输出型参数, 可以输出_listensock监听到的用户的信息,其中包含用户的IP地址和端口号等
// socklen_t *addrlen :输出型参数,代表要传递的参数struct sockaddr *addr的长度
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
// 获取连接失败,并不是致命的错误,因此日志等级为ERROR
logMessage(ERROR, "accept error, next");
// 获取连接失败,则重复上面代码的运行,继续获取_listensock监听的下一个用户的连接
continue;
}
logMessage(NORMAL, "accept a new link success, get new sock: %d", sock);
// version4 线程池版
// 将构建的任务push到线程池的任务队列中
// Task(sock, serviceIO) 就是构建的任务,是一个匿名对象
ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));
}
}
private:
// 不是用来进行数据通信的,它是用来监听链接到来,获取新链接的
int _listensock;
uint16_t _port;
};
} // namespace server
tcpServer.cc
c
#include "tcpServer.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
tsvr->start();
return 0;
}
tcpClient.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define NUM 1024
class TcpClient
{
public:
// 客户端的构造函数
TcpClient(const std::string &serverip, const uint16_t &serverport)
: _sock(-1), _serverip(serverip), _serverport(serverport)
{}
// 初始化客户端
void initClient()
{
// 1. 创建套接字socket
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
// 创建套接字失败,打印错误信息
std::cerr << "socket create error" << std::endl;
exit(2);
}
// 2. tcp的客户端要不要bind?要的!
// 要不要显示的bind?不要!这里尤其是client port要让OS自定随机指定!
// 3. 要不要listen?不要!
// 4. 要不要accept? 不要!
// 5. 要什么呢? 要发起链接!
}
// 启动客户端
void start()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(_serverport);
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
if(connect(_sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
// 客户端向服务器发起链接失败,则打印错误信息
std::cerr << "socket connect error" << std::endl;
}
else
{
// 链接成功,客户端和服务端进行通信
std::string msg;
while(true)
{
std::cout << "Enter# ";
std::getline(std::cin, msg);
write(_sock, msg.c_str(), msg.size());
char buffer[NUM];
int n = read(_sock, buffer, sizeof(buffer)-1);
if(n > 0)
{
//目前我们把读到的数据当成字符串, 截止目前
buffer[n] = 0;
std::cout << "Server回显# " << buffer << std::endl;
}
else
{
break;
}
}
}
}
// 客户端的析构函数
~TcpClient()
{
// 关闭客户端套接字对应的文件描述符
if(_sock >= 0) close(_sock);
}
private:
int _sock;
std::string _serverip;
uint16_t _serverport;
};
tcpClient.cc
c
#include "tcpClient.hpp"
#include <memory>
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<TcpClient> tcli(new TcpClient(serverip, serverport));
tcli->initClient();
tcli->start();
return 0;
}
演示结果

守护进程
&(将进程改变为后台进程)
jobs 命令(查看后台运行的命令)
在Linux中,jobs命令用于显示当前终端会话中正在运行的作业(即在后台运行的命令)。它会列出作业的编号和状态。下面是一个演示:
-
打开终端。
-
在终端中输入一个长时间运行的命令,例如
sleep 10
,然后按下Enter键。这个命令会在后台运行10秒钟。 -
在终端中输入
jobs
命令,然后按下Enter键。您将看到类似以下的输出:[1]+ Running sleep 10 &
这表示作业编号为1的作业正在后台运行。
-
等待10秒钟,然后再次输入
jobs
命令,按下Enter键。您将看到类似以下的输出:[1]+ Done sleep 10
这表示作业编号为1的作业已经完成。
-
如果您在第2步中使用了
sleep 10 &
命令,即在命令末尾添加了&符号,那么您可以使用fg
命令将作业切换到前台运行。输入fg %1
命令,然后按下Enter键。这将把作业编号为1的作业切换到前台运行。 -
按下Ctrl+C键来终止作业的运行。
这是一个简单的演示,展示了如何使用jobs命令来查看和管理后台作业。请注意,jobs命令只能在当前终端会话中显示作业,如果您打开了多个终端会话,则需要在相应的终端中使用jobs命令来查看该终端会话中的作业。
ctrl+z(暂停前台进程)
是的,在Linux中,使用Ctrl+Z组合键可以暂停当前正在前台运行的进程。当您按下Ctrl+Z时,终端会发送一个SIGTSTP信号给正在运行的进程,使其暂停执行。
下面是一个演示:
-
打开终端。
-
在终端中输入一个长时间运行的命令,例如
sleep 10
,然后按下Enter键。这个命令会在前台运行,终端会被阻塞10秒钟。 -
在终端中按下Ctrl+Z组合键。您将看到类似以下的输出:
[1]+ Stopped sleep 10
这表示作业编号为1的作业已经被暂停。
-
您可以使用
jobs
命令查看当前暂停的作业。输入jobs
命令,然后按下Enter键。您将看到类似以下的输出:[1]+ Stopped sleep 10
这表示作业编号为1的作业已经被暂停。
- 如果您想恢复暂停的作业,可以使用
fg
命令。输入fg
命令,然后按下Enter键。作业将会恢复执行,并且终端会再次被阻塞,直到作业完成。
通过使用Ctrl+Z组合键,您可以暂停正在前台运行的进程,并在需要时恢复它们的执行。这对于需要暂停和恢复进程的情况非常有用。
bg(将一个被暂停的作业(通过Ctrl+Z暂停的作业)放到后台继续执行)
在Linux中,bg
命令用于将一个被暂停的作业(通过Ctrl+Z暂停的作业)放到后台继续执行。
下面是使用bg
命令的示例:
-
打开终端。
-
在终端中输入一个长时间运行的命令,例如
sleep 10
,然后按下Enter键。这个命令会在前台运行,终端会被阻塞10秒钟。 -
在终端中按下Ctrl+Z组合键。您将看到类似以下的输出:
[1]+ Stopped sleep 10
这表示作业编号为1的作业已经被暂停。
-
输入
bg
命令,然后按下Enter键。作业将会被放到后台继续执行。 -
您可以使用
jobs
命令查看当前正在运行的作业。输入jobs
命令,然后按下Enter键。您将看到类似以下的输出:[1]+ Running sleep 10 &
这表示作业编号为1的作业正在后台运行。
- 终端不再被阻塞,您可以继续输入其他命令。
通过使用bg
命令,您可以将一个被暂停的作业放到后台继续执行,从而释放终端并允许您执行其他任务。
演示以上命令
c
[qwy@VM-4-17-centos lesson43]$ [NORMAL][1695132660][pid: 5028]create socket success: 3
[NORMAL][1695132660][pid: 5028]bind socket success
[NORMAL][1695132660][pid: 5028]create socket success: 3
[DEBUG][1695132660][pid: 5028]thread-1 start ...
[DEBUG][1695132660][pid: 5028]thread-2 start ...
[DEBUG][1695132660][pid: 5028]thread-3 start ...
[DEBUG][1695132660][pid: 5028]thread-4 start ...
[DEBUG][1695132660][pid: 5028]thread-5 start ...
[DEBUG][1695132660][pid: 5028]thread-6 start ...
[DEBUG][1695132660][pid: 5028]thread-7 start ...
[DEBUG][1695132660][pid: 5028]thread-8 start ...
[DEBUG][1695132660][pid: 5028]thread-9 start ...
[DEBUG][1695132660][pid: 5028]thread-10 start ...
[NORMAL][1695132660][pid: 5028]Thread init success
[qwy@VM-4-17-centos lesson43]$ jobs
[1]+ Running ./tcpserver 8080 &
[qwy@VM-4-17-centos lesson43]$ sleep 10000 | sleep 20000 | sleep 30000 &
[2] 5371
[qwy@VM-4-17-centos lesson43]$ jobs
[1]- Running ./tcpserver 8080 &
[2]+ Running sleep 10000 | sleep 20000 | sleep 30000 &
[qwy@VM-4-17-centos lesson43]$ sleep 40000 | sleep 50000 | sleep 60000 &
[3] 5506
[qwy@VM-4-17-centos lesson43]$ jobs
[1] Running ./tcpserver 8080 &
[2]- Running sleep 10000 | sleep 20000 | sleep 30000 &
[3]+ Running sleep 40000 | sleep 50000 | sleep 60000 &
[qwy@VM-4-17-centos lesson43]$ ps ajx | head -n1 && ps ajx | grep sleep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4321 4497 1373 1373 ? -1 Sl 0 4:23 /bin/sh -c sleep 100
5228 5369 5369 5228 pts/0 6019 S 1001 0:00 sleep 10000
5228 5370 5369 5228 pts/0 6019 S 1001 0:00 sleep 20000
5228 5371 5369 5228 pts/0 6019 S 1001 0:00 sleep 30000
5228 5504 5504 5228 pts/0 6019 S 1001 0:00 sleep 40000
5228 5505 5504 5228 pts/0 6019 S 1001 0:00 sleep 50000
5228 5506 5504 5228 pts/0 6019 S 1001 0:00 sleep 60000
25002 5752 25002 25002 ? -1 S 1001 0:00 sleep 180
5228 6020 6019 5228 pts/0 6019 S+ 1001 0:00 grep --color=auto sleep
[qwy@VM-4-17-centos lesson43]$ jobs
[1] Running ./tcpserver 8080 &
[2]- Running sleep 10000 | sleep 20000 | sleep 30000 &
[3]+ Running sleep 40000 | sleep 50000 | sleep 60000 &
[qwy@VM-4-17-centos lesson43]$ fg 2
sleep 10000 | sleep 20000 | sleep 30000
^Z
[2]+ Stopped sleep 10000 | sleep 20000 | sleep 30000
[qwy@VM-4-17-centos lesson43]$ jobs
[1] Running ./tcpserver 8080 &
[2]+ Stopped sleep 10000 | sleep 20000 | sleep 30000
[3]- Running sleep 40000 | sleep 50000 | sleep 60000 &
[qwy@VM-4-17-centos lesson43]$ bg 2
[2]+ sleep 10000 | sleep 20000 | sleep 30000 &
[qwy@VM-4-17-centos lesson43]$ jobs
[1] Running ./tcpserver 8080 &
[2]- Running sleep 10000 | sleep 20000 | sleep 30000 &
[3]+ Running sleep 40000 | sleep 50000 | sleep 60000 &


图解

SIGPIPE的使用方法
在Linux中,SIGPIPE是一个信号,用于处理管道破裂的情况。当一个进程向一个已经关闭写端的管道写入数据时,内核会向该进程发送SIGPIPE信号。默认情况下,进程会终止并退出。
SIGPIPE信号的处理方式可以通过以下几种方式进行设置:
-
忽略信号:可以使用
signal(SIGPIPE, SIG_IGN)
函数将SIGPIPE信号设置为忽略。这样,当进程向一个已经关闭写端的管道写入数据时,不会终止进程,而是返回一个错误。 -
自定义处理函数:可以使用
signal(SIGPIPE, handler)
函数将SIGPIPE信号设置为自定义处理函数。在处理函数中,您可以根据需要执行特定的操作,例如记录日志、重新打开管道等。 -
恢复默认行为:可以使用
signal(SIGPIPE, SIG_DFL)
函数将SIGPIPE信号恢复为默认行为。这样,当进程向一个已经关闭写端的管道写入数据时,进程会终止并退出。
以下是一个示例程序,演示了如何设置SIGPIPE信号的处理方式:
c
#include <stdio.h>
#include <signal.h>
void sigpipe_handler(int signum) {
printf("Received SIGPIPE signal\n");
// 自定义处理逻辑
}
int main() {
// 设置SIGPIPE信号的处理函数为自定义处理函数
signal(SIGPIPE, sigpipe_handler);
// 向一个已经关闭写端的管道写入数据
int pipefd[2];
pipe(pipefd);
close(pipefd[1]);
write(pipefd[0], "Hello", 5);
return 0;
}
在上述示例中,当向一个已经关闭写端的管道写入数据时,会触发SIGPIPE信号,并调用自定义的处理函数sigpipe_handler
。在处理函数中,我们简单地打印了一条消息。
请注意,SIGPIPE信号的处理方式对于不同的应用场景可能会有所不同。您可以根据具体需求选择适合的处理方式。
在Linux中,pipe()
是一个系统调用函数,用于创建一个管道。管道是一种特殊的文件,用于在两个进程之间进行通信。
pipe()
函数的原型如下:
c
#include <unistd.h>
int pipe(int pipefd[2]);
参数pipefd
是一个整型数组,用于存储管道的读端和写端的文件描述符。pipefd[0]
表示管道的读端,pipefd[1]
表示管道的写端。
pipe()
函数成功创建管道时,返回0;失败时,返回-1,并设置errno来指示错误的原因。
以下是一个示例程序,演示了如何使用pipe()
函数创建管道并进行进程间通信:
c
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
char buffer[20];
// 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程,关闭写端,从管道中读取数据
close(pipefd[1]);
read(pipefd[0], buffer, sizeof(buffer));
printf("Child process received: %s\n", buffer);
close(pipefd[0]);
} else {
// 父进程,关闭读端,向管道中写入数据
close(pipefd[0]);
write(pipefd[1], "Hello from parent", 17);
close(pipefd[1]);
}
return 0;
}
在上述示例中,首先使用pipe()
函数创建了一个管道。然后,通过fork()
函数创建了一个子进程。在子进程中,关闭了管道的写端,然后从管道中读取数据并打印。在父进程中,关闭了管道的读端,然后向管道中写入数据。
运行该程序,可以看到子进程接收到了父进程发送的数据,并打印出来。
需要注意的是,管道是一种半双工的通信方式,数据只能在一个方向上流动。因此,在使用管道进行进程间通信时,需要注意读写端的正确使用。
setsid()
c
NAME
setsid - creates a session and sets the process group ID
SYNOPSIS
#include <unistd.h>
pid_t setsid(void);
DESCRIPTION
setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new session, the process group leader of the new process group, and has no controlling terminal. The process group ID and ses‐sion ID of the calling process are set to the PID of the calling process. The calling process will be the only process in this new process group and in this new session.
// 如果调用进程不是当前进程组领导,Setsid()将创建一个新会话。调用进程是新会话的leader,是新进程组的进程组leader,没有控制终端。调用进程的进程组ID和会话ID设置为调用进程的PID。调用进程将是此新进程组和此新会话中的唯一进程。
RETURN VALUE
On success, the (new) session ID of the calling process is returned. On error, (pid_t) -1 is returned, and errno is set to indicate the error.
// 如果成功,则返回调用进程的(新)会话ID。当出现错误时,返回(pid_t) -1,并设置errno来指示错误。
查看进程的执行路径

chdir()
c
chdir, fchdir - change working directory
SYNOPSIS
#include <unistd.h>
int chdir(const char *path);
DESCRIPTION
chdir() changes the current working directory of the calling process to the directory specified in path.
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
演示结果
daemon.hpp
c
#pragma once
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"
void daemonSelf(const char *currPath = nullptr)
{
// 1. 让调用进程忽略掉异常的信号
// 防止服务器收到异常信号被终止
signal(SIGPIPE, SIG_IGN);
// 守护进程不能够是任务组的组长(组长跑路了,组员怎么办,所以组长不能走)
// 2. 如何让自己不是组长,只需要自己创建一个子进程,那么创建的这个子进程
// 肯定不会是当前进程组的组长,再由这个子进程调用setsid()来创建一个新会话即可
if (fork() > 0)
exit(0);
// 子进程 -- 守护进程,精灵进程,本质就是孤儿进程的一种!
pid_t n = setsid();
assert(n != -1);
// 3. 守护进程是脱离终端的,关闭或者重定向以前进程默认打开的文件
// 因为守护进程是脱离终端的,所以键盘和显示器对应的0~2号的文件描述符
// 守护进程需要将其闭或者重定向
// 比较温和的做法,就是进行重定向,这样守护进程还可以打印日志,且不会打印到显示器上
// /dev/null 这个路径的这个文件,我们可以将其理解为是一个文件黑洞
// 如果向这个文件进行写入,这个文件的默认处理方式是会把写入的数据进行丢弃
// O_RDWR 代表以写入的方式打开这个文件
int fd = open(DEV, O_RDWR);
if (fd >= 0)
{
// int dup2(int oldfd, int newfd);
// dup2() makes newfd be the copy of oldfd
// 那么向0,1,2对应的文件描述符进行读写,就会将其重定向到fd文件描述符对应的文件
// 使用open()打开/dev/null,是为了得到文件对应的文件描述符
// 因此重定向之后,就可以将fd对应的文件关闭了
// (已经重定向好了,不是直接向fd对应的文件进行写入,因此这个文件不需要进行打开)
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
else
{
close(0);
close(1);
close(2);
}
// 4. 可选:进程执行路径发生更改
// 使用chdir()函数改变当前进程的执行路径(一般进程执行的路径都是在当前目录下)
if (currPath)
chdir(currPath);
}
tcpServer.cc
c
#include "tcpServer.hpp"
#include "daemon.hpp"
#include <memory>
using namespace server;
using namespace std;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// tcp服务器,启动上和udp server一模一样
// ./tcpserver local_port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
// 在服务器启动之前,使其成为守护进程
daemonSelf();
tsvr->start();
return 0;
}

log.hpp
c
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// 将宏对应的数字,以字符串的形式进行返回
const char *to_levelstr(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
// prefix n.前缀
// 日志的前缀,将其打印到缓冲区logprefix中
char logprefix[NUM];
// to_levelstr(level), 错误等级
// (long int)time(nullptr), 时间戳,错误发生的时间
// getpid(),哪个进程调用的这个函数,就会打印其pid
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
// 此时,arg指向的就是format
va_start(arg, format);
// 将可变参数列表arg中的参数,按照format中的格式,打印到缓冲区ogcontent中
vsnprintf(logcontent, sizeof(logcontent), format, arg);
// 将日志打印到对应的文件中,而不是0,1,2文件描述符对应的文件中,那么即使服务器变为了守护进程,也不会影响日志的打印
FILE *log = fopen(LOG_NORMAL, "a");
FILE *err = fopen(LOG_ERR, "a");
if (log != nullptr && err != nullptr)
{
FILE *curr = nullptr;
if (level == DEBUG || level == NORMAL || level == WARNING)
curr = log;
if (level == ERROR || level == FATAL)
curr = err;
if (curr)
fprintf(curr, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(err);
}
}
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给服务器; (第四次)
-
这个断开连接的过程, 通常称为 四次挥手

TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(port));
tsvr->initServer();
// 在服务器启动之前,使其成为守护进程
daemonSelf();
tsvr->start();
return 0;
}
[外链图片转存中...(img-tZy67J5Q-1743573700002)]
## log.hpp
```c
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// 将宏对应的数字,以字符串的形式进行返回
const char *to_levelstr(int level)
{
switch (level)
{
case DEBUG:
return "DEBUG";
case NORMAL:
return "NORMAL";
case WARNING:
return "WARNING";
case ERROR:
return "ERROR";
case FATAL:
return "FATAL";
default:
return nullptr;
}
}
void logMessage(int level, const char *format, ...)
{
#define NUM 1024
// prefix n.前缀
// 日志的前缀,将其打印到缓冲区logprefix中
char logprefix[NUM];
// to_levelstr(level), 错误等级
// (long int)time(nullptr), 时间戳,错误发生的时间
// getpid(),哪个进程调用的这个函数,就会打印其pid
snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
to_levelstr(level), (long int)time(nullptr), getpid());
char logcontent[NUM];
va_list arg;
// 此时,arg指向的就是format
va_start(arg, format);
// 将可变参数列表arg中的参数,按照format中的格式,打印到缓冲区ogcontent中
vsnprintf(logcontent, sizeof(logcontent), format, arg);
// 将日志打印到对应的文件中,而不是0,1,2文件描述符对应的文件中,那么即使服务器变为了守护进程,也不会影响日志的打印
FILE *log = fopen(LOG_NORMAL, "a");
FILE *err = fopen(LOG_ERR, "a");
if (log != nullptr && err != nullptr)
{
FILE *curr = nullptr;
if (level == DEBUG || level == NORMAL || level == WARNING)
curr = log;
if (level == ERROR || level == FATAL)
curr = err;
if (curr)
fprintf(curr, "%s%s\n", logprefix, logcontent);
fclose(log);
fclose(err);
}
}
TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程:
外链图片转存中...(img-tJTWqY4L-1743573700002)
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
- 这个建立连接的过程, 通常称为 三次握手;
外链图片转存中...(img-OiNw8N0I-1743573700003)
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去
断开连接的过程:
-
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
-
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
-
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
-
客户端收到FIN, 再返回一个ACK给服务器; (第四次)
-
这个断开连接的过程, 通常称为 四次挥手
外链图片转存中...(img-Aakh3dOG-1743573700003)
TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报