文章目录
- 1.API介绍
- 1.1wc -l dirName
- 1.2inet_pton
- 1.3inet_aton
- 1.4inet_ntop
- 2.三次握手与四次挥手
- 1.三次握手
- 2.四次挥手
- 3.应用程序和TCP协议层如何交互
- 总结
- 3.TCP 和 UDP 对比
- 1.宏观
- 2.详细
- 4.地址转换函数
- inet_ntoa
- 5.TCP编程代码
- Makefile
- tcp_client.cc
- tcp_server.cc
- tcp_server.hpp
- 线程池
- Log.hpp
- LockGuard.hpp
- singleThreadPool.hpp
- Task.hpp
- thread.hpp
1.API介绍
1.1wc -l dirName
wc -l *
wc -l ./dirName/*
1.2inet_pton
int inet_pton(int af, const char *src, void *dst);
inet_pton 是一个在 Linux 系统和许多其他类 Unix 系统中用于处理网络地址转换的函数。它可以将点分十进制的 IP 地址(IPv4)或冒分十六进制的 IP 地址(IPv6)转换为二进制格式。这个函数在处理网络编程时非常有用,因为它允许你轻松地将人类可读的 IP 地址转换为程序可以使用的格式。
函数参数
int af: 地址族。它定义了目标地址的格式。通常,它可以是以下值之一:
AF_INET: 表示目标地址是 IPv4 地址。
AF_INET6: 表示目标地址是 IPv6 地址。
*const char src: 指向包含要转换的 IP 地址的字符串的指针。这个字符串应该是一个以空字符结尾的字符串,其格式取决于 af 参数的值。
*void dst: 指向存储转换后的二进制地址的缓冲区的指针。这个缓冲区的大小应该足够大,以容纳转换后的地址。对于 IPv4,缓冲区至少应该为 4 字节;对于 IPv6,缓冲区至少应该为 16 字节。
函数功能
inet_pton 函数的功能是将点分十进制或冒分十六进制的 IP 地址字符串转换为相应的二进制格式,并将结果存储在提供的目标缓冲区中。
函数工作原理
函数的工作原理大致如下:
根据 af 参数的值确定目标地址的格式(IPv4 或 IPv6)。
解析 src 指向的字符串,将其从人类可读的格式转换为二进制格式。
将转换后的二进制地址存储在 dst 指向的缓冲区中。
返回值
inet_pton 函数的返回值表示转换是否成功:
如果转换成功,函数返回 1。
如果输入的字符串不是有效的 IP 地址,函数返回 0。
如果发生错误(例如,无效的 af 参数或 dst 缓冲区太小),函数返回 -1。
示例
下面是一个简单的示例,展示如何使用 inet_pton 函数将 IPv4 地址字符串转换为二进制格式:
c
#include <stdio.h>
#include <arpa/inet.h>
int main() {
const char *ip_str = "192.168.1.1";
struct in_addr ip_bin;//in_addr: struct sockaddr_in 里用于存储ip的结构体字段
if (inet_pton(AF_INET, ip_str, &ip_bin) == 1) {
printf("Binary IP address: %u\n", ip_bin.s_addr);
} else {
printf("Invalid IP address or error occurred.\n");
}
return 0;
}
在这个示例中,我们将字符串 “192.168.1.1” 转换为二进制格式,并打印结果。注意,我们使用了 struct in_addr 结构体来存储 IPv4 地址的二进制表示。
1.3inet_aton
int inet_aton(const char *cp, struct in_addr *inp);
inet_aton 是 Linux 系统和其他类 Unix 系统中用于将点分十进制的 IPv4 地址转换为网络字节序的二进制形式的函数。这个函数在处理网络编程时非常有用,因为它允许开发者将人类可读的 IP 地址字符串转换为计算机可以直接使用的格式。
函数参数
*const char cp: 指向包含要转换的 IPv4 地址字符串的指针。这个字符串应该是一个以空字符结尾的字符串,并且应该符合点分十进制的格式,例如 “192.168.1.1”。
*struct in_addr inp: 指向存储转换后的二进制地址的 struct in_addr 结构体的指针。这个结构体用于存储 IPv4 地址的二进制表示。
函数功能
inet_aton 函数的功能是将点分十进制的 IPv4 地址字符串转换为网络字节序的二进制形式,并将结果存储在提供的 struct in_addr 结构体中。
函数工作原理
函数的工作原理大致如下:
解析 cp 指向的字符串,检查它是否符合点分十进制的 IPv4 地址格式。
如果字符串是有效的 IPv4 地址,函数将其转换为网络字节序的二进制形式。网络字节序是大端字节序,即高位字节存储在内存的低地址端,低位字节存储在内存的高地址端。
将转换后的二进制地址存储在 inp 指向的 struct in_addr 结构体中。
返回值
inet_aton 函数的返回值是一个整型值,用于表示转换是否成功:
如果转换成功,函数返回非零值(通常是 1)。
如果输入的字符串不是有效的 IPv4 地址,函数返回零。
示例
下面是一个简单的示例,展示如何使用 inet_aton 函数将 IPv4 地址字符串转换为二进制格式:
c
#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
const char *ip_str = "192.168.1.1";
struct in_addr ip_bin;
if (inet_aton(ip_str, &ip_bin)) {
unsigned long ip_num = ip_bin.s_addr; // 获取网络字节序的IP地址
printf("Binary IP address (in network byte order): %lu\n", ip_num);
} else {
printf("Invalid IP address.\n");
}
return 0;
}
在这个示例中,我们将字符串 “192.168.1.1” 转换为网络字节序的二进制格式,并打印结果。注意,struct in_addr 结构体中的 s_addr 成员用于存储网络字节序的 IPv4 地址。如果需要将其转换为主机字节序(即小端字节序),可以使用 ntohl 函数。
1.4inet_ntop
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
inet_ntop 是 Linux 和其他类 Unix 系统中用于将网络地址从数值格式转换为人类可读的字符串格式的函数。它是 inet_pton 的反向操作,用于将二进制格式的 IP 地址转换回点分十进制(IPv4)或冒分十六进制(IPv6)字符串表示。
函数参数
int af: 地址族。它定义了源地址的格式。常用的值有:
AF_INET: 表示源地址是 IPv4 地址。
AF_INET6: 表示源地址是 IPv6 地址。
*const void src: 指向包含要转换的二进制地址的缓冲区的指针。这个缓冲区应该包含由 inet_pton 或其他方法设置的有效的二进制地址。
*char dst: 指向存储转换后的字符串的缓冲区的指针。这个缓冲区应该足够大,以容纳转换后的字符串,包括末尾的空字符。对于 IPv4 地址,缓冲区大小至少应为 INET_ADDRSTRLEN(通常为 16 字节);对于 IPv6 地址,缓冲区大小至少应为 INET6_ADDRSTRLEN(通常为 46 字节)。
socklen_t size: 指定 dst 缓冲区的大小。这有助于防止缓冲区溢出。
函数功能
inet_ntop 的功能是将二进制格式的 IP 地址转换为点分十进制(IPv4)或冒分十六进制(IPv6)的字符串表示。
函数工作原理
函数的工作原理大致如下:
根据 af 参数的值确定源地址的格式(IPv4 或 IPv6)。
解析 src 指向的二进制缓冲区,将其从二进制格式转换为人类可读的字符串格式。
将转换后的字符串存储在 dst 指向的缓冲区中,并在字符串末尾添加一个空字符。
如果 dst 缓冲区太小,无法容纳完整的字符串(包括末尾的空字符),则函数可能无法正常工作,或者结果可能被截断。
返回值
inet_ntop 函数的返回值是一个指向转换后的字符串的指针。如果转换成功,该指针将指向 dst 缓冲区中的字符串。如果发生错误(例如,无效的 af 参数或 dst 缓冲区太小),函数将返回 NULL。
示例
下面是一个简单的示例,展示如何使用 inet_ntop 函数将 IPv4 地址的二进制表示转换为点分十进制字符串:
c
#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
struct in_addr ip_bin = { .s_addr = 0x01020304 }; // 示例的 IPv4 地址(二进制格式)
char ip_str[INET_ADDRSTRLEN]; // 用于存储转换后的字符串的缓冲区
const char *result = inet_ntop(AF_INET, &ip_bin, ip_str, sizeof(ip_str));
if (result != NULL) {
printf("IP address in human-readable format: %s\n", ip_str);
} else {
perror("inet_ntop failed");
return 1;
}
return 0;
}
在这个示例中,我们创建了一个包含示例 IPv4 地址的 struct in_addr 结构体,并使用 inet_ntop 将其转换为点分十进制的字符串表示。然后,我们打印出转换后的字符串。如果 inet_ntop 函数失败,我们会打印一个错误消息。
2.三次握手与四次挥手
1.三次握手
服务器初始化
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
调用socket, 创建文件描述符;
调用connect, 向服务器发起连接请求;connect会发出SYN段并阻塞等待服务器应答; (第一次)
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
2.四次挥手
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
3.应用程序和TCP协议层如何交互
- 应用程序调用某个socket函数时,TCP协议层会完成什么动作:比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如:从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。
总结
TCP的三次握手与四次挥手是TCP/IP协议中用于建立和维护可靠传输连接的重要过程。
三次握手:
第一次握手:客户端向服务器发送一个SYN包,并等待服务器确认。此时,客户端进入SYN_SENT状态。
第二次握手:服务器收到SYN包后,向客户端发送一个SYN/ACK包,确认客户端的SYN包,并请求客户端确认。此时,服务器进入SYN_RECV状态。
第三次握手:客户端收到SYN/ACK包后,向服务器发送一个ACK包,表示已完成连接。此时,客户端和服务器都进入ESTABLISHED状态,完成TCP三次握手。
三次握手的主要目的是确认双方的通信能力和理解能力,确保双方的初始序列号一致,以便后续的可靠传输。通过三次握手,TCP连接得以建立,数据可以开始传输。
四次挥手:
四次挥手主要用于终止一个已经建立的TCP连接。这个过程由TCP的半关闭特性决定,即TCP连接的一端在结束发送后还能接收来自另一端的数据。
第一次挥手:客户端发送一个FIN包给服务器,请求关闭连接。此时,客户端进入FIN_WAIT_1状态。
第二次挥手:服务器收到FIN包后,发送一个ACK包给客户端,确认收到关闭请求。此时,服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
第三次挥手:服务器在关闭连接前发送一个FIN包给客户端,请求关闭连接。此时,服务器进入LAST_ACK状态。
第四次挥手:客户端收到FIN包后,发送一个ACK包给服务器,确认收到关闭请求。此时,客户端进入TIME_WAIT状态,等待一段时间后进入CLOSED状态,服务器收到ACK包后也进入CLOSED状态,完成TCP四次挥手。
四次挥手的主要目的是确保在关闭连接前,双方都能完成数据的传输和确认,避免数据丢失或重复传输。
总的来说,TCP的三次握手和四次挥手是TCP协议中用于建立和维护可靠传输连接的重要机制,它们确保了数据的可靠传输和完整性。
3.TCP 和 UDP 对比
1.宏观
可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 vs 数据报
2.详细
- 客户端不是不允许调用bind(), 只是没有必要调用bind()固定一个端口号. 否则如果在同一台机器上启动多个客户端, 就会出现端口号被占用导致不能正确建立连接;
- 服务器也不是必须调用bind(), 但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样, 客户端要连接服务器就会遇到麻烦;
- 客户端需要调用connect()连接服务器;connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址; connect()成功返回0,出错返回-1;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
4.地址转换函数
基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
#include <arpa/inet.h>
int inet_aton(const char *strptr,struct in addr *addrptr);
in_addr_t inet_addr(const char*strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
in_addr转字符串的函数
char *inet_ntoa(struct in addr inaddr);
const char *inet_ntop(int family,const void *addrptr, char *strptr, size t len);
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr
inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
如果我们调用多次这个函数, 会有什么样的效果呢?为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
in_port_t sin_port;
struct in_addr sin_addr;
};
*/
// in_addr转字符串
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
char *ptr1 = inet_ntoa(addr1.sin_addr);
char *ptr2 = inet_ntoa(addr2.sin_addr);
printf("ptr1:%s, ptr2:%s\n", ptr1, ptr2);
return 0;
}
- 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在APUE(Advanced Programming in the UNIX Environment[一本书])中, 明确提出inet_ntoa不是线程安全的函数;
- 在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题.
centos7 测试
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
int count = 5;
while (count--)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr1: %s\n", ptr);
}
return NULL;
}
void *Func2(void *p)
{
struct sockaddr_in *addr = (struct sockaddr_in *)p;
int count = 5;
while (count--)
{
char *ptr = inet_ntoa(addr->sin_addr);
printf("addr2: %s\n", ptr);
}
return NULL;
}
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_t tid1 = 0;
pthread_create(&tid1, NULL, Func1, &addr1);
pthread_t tid2 = 0;
pthread_create(&tid2, NULL, Func2, &addr2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
5.TCP编程代码
Makefile
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++11
tcp_server:tcp_server.cc
g++ -o $@ $^ -std=c++11 -lpthread -DDEBUG_COMPILE
.PHONY:clean
clean:
rm -f tcp_client tcp_server
tcp_client.cc
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
static void usage(std::string proc)
{
std::cout << std::endl
<< "Usage: " << proc << " port" << std::endl
<< std::endl;
}
//服务端的字典服务实现的是短服务 即为一个客户端服务一次后 该服务停止 客户端再次申请服务时必须再次socket/connect
// ./tcp_client targetIp targetPort
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverIp = argv[1];
uint16_t serverPort = atoi(argv[2]);
volatile bool isAlive = false;
int socketFd = 0;
std::string line;
char buf[1024];
while (true)
{
if (isAlive == false)
{
socketFd = socket(AF_INET, SOCK_STREAM, 0);
if (socketFd < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// client 不需要显示的bind 他肯定是需要port
// client的port让os自动选择 从而有连接别人的能力
struct sockaddr_in dst_sockAddr;
memset(&dst_sockAddr, 0, sizeof(dst_sockAddr));
dst_sockAddr.sin_family = AF_INET;
dst_sockAddr.sin_addr.s_addr = inet_addr(serverIp.c_str());
dst_sockAddr.sin_port = htons(serverPort);
if (connect(socketFd, (struct sockaddr *)&dst_sockAddr, sizeof(dst_sockAddr)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl;
isAlive = true;
}
std::cout << "请输入# ";
std::cin >> std::ws; // 跳过前导空白字符
std::getline(std::cin, line);
if (line == "quit")
{
close(socketFd);
isAlive = false;
break;
}
ssize_t sendBytes = send(socketFd, line.c_str(), line.size(), 0);
sleep(1); // 等待缓冲区中的数据发送过去
if (sendBytes < 0)
{
std::cerr << "send error!" << std::endl;
close(socketFd);
isAlive = false;
continue;
}
memset(buf, 0, sizeof buf);
ssize_t recvBytes = recv(socketFd, buf, sizeof(buf) - 1, 0); // typedef ssize_t long int
if (recvBytes > 0)
{
buf[recvBytes] = 0;
std::cout << "server echo# " << buf << std::endl;
}
else
{
std::cerr << "recv error!" << std::endl;
close(socketFd);
isAlive = false;
}
}
return 0;
}
tcp_server.cc
#include <memory>
#include "tcp_server.hpp"
static void usage(std::string proc)
{
std::cout << std::endl
<< "Usage: " << proc << " port" << std::endl
<< std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TcpServer> unqPtrSvr(new TcpServer(port));
unqPtrSvr->initServer();
unqPtrSvr->start();
return 0;
}
tcp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cctype>
#include <memory>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 1.头文件引用 如果是.h和.c分开写 Makefile里要修改
// 2.打包成静态库/动态库使用
// 3.头文件 路径引用 .hpp
#include "singleThreadPool/Log.hpp"
#include "singleThreadPool/singleThreadPool.hpp"
#include "singleThreadPool/Task.hpp"
static void echoService(int serviceSocket, const std::string &clientip, const uint16_t &clientport)
{
char buf[1024];
while (true)
{
// 在服务端显示客户端发送的信息
ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0; // 将发过来的数据当做字符串
std::cout << clientip << ":" << clientport << "# " << buf << std::endl;
}
else if (s == 0) // 对方停止写入
{
logMsg(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{
logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
break;
}
// 把客户端发的信息再传回去
write(serviceSocket, buf, strlen(buf));
}
close(serviceSocket);
}
// 当前我们的服务器是一旦有客户端申请服务 服务端连接上后 只要客户端不退出 服务端就一直进行服务如回显他的消息
// 引进线程池处理任务后 对于这种长连接的服务 如果同时有超过g_threadNum个客户端申请服务
// 线程池里的线程就不够用了 客户端发来的服务请求即任务就会先放到队列里 知道有客户端服务结束退出
// 知识:线程数量要有上限 否则如果同时有大量客户申请服务 此时OS中的线程数量就会剧增 效率降低是一方面
// OS为了为新的客户提供服务 可能会停止先前的进程 这对OS是不好的
// 解决:
// 1. 参考OS设计的进程/线程调度问题 给一个客户服务长达一定时间 就停止对他服务 转向服务别人 一段时间后再来对他服务
// 2.如果就是想要长期进行服务 这个问题后续再讲解
static void Task_echo(int serviceSocket, const std::string &clientip,
const uint16_t &clientport, const std::string &threadName)
{
char buf[1024];
while (true)
{
// 在服务端显示客户端发送的信息
ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0; // 将发过来的数据当做字符串
std::cout << threadName << "|" << clientip << ":" << clientport << " client send# " << buf << std::endl;
}
else if (s == 0) // 对方停止写入或关闭连接
{
logMsg(NORMAL, "%s:%d client shutdown, me too!", clientip.c_str(), clientport);
break;
}
else
{
logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
break;
}
// 把客户端发的信息再传回去
write(serviceSocket, buf, strlen(buf));
}
close(serviceSocket);
}
// 大小写转换
static void Task_toggle(int serviceSocket, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
char buf[1024];
ssize_t s = read(serviceSocket, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0; // 发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << " client send# " << buf << std::endl;
std::string msg;
char *start = buf;
while (*start)
{
char character;
if (islower(*start))
character = toupper(*start);
else
character = *start;
msg.push_back(character);
start++;
}
write(serviceSocket, msg.c_str(), msg.size());
}
else if (s == 0) // 对方停止写入或关闭连接
{
logMsg(NORMAL, "%s:%d client shutdown, me too!", clientip.c_str(), clientport);
}
else
{
logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
}
close(serviceSocket);
}
static void Task_Dictionary(int serviceSocket, const std::string &clientip,
const uint16_t &clientport, const std::string &thread_name)
{
static std::unordered_map<std::string, std::string> dictionary =
{
{"apple", "苹果"},
{"banana", "香蕉"},
{"hard", "好难啊"}};
char EnglishBuf[1024];
ssize_t readBytes = read(serviceSocket, EnglishBuf, sizeof(EnglishBuf) - 1);
if (readBytes > 0)
{
EnglishBuf[readBytes] = 0; // 将发过来的数据当做字符串
std::cout << thread_name << "|" << clientip << ":" << clientport << "|"
<< " client send# " << EnglishBuf << std::endl;
std::string msg;
auto iter = dictionary.find(EnglishBuf);
if (iter == dictionary.end())
msg = "unknown";
else
msg = iter->second;
write(serviceSocket, msg.c_str(), msg.size());
}
else if (readBytes == 0) // 对方停止写入或关闭连接
{
logMsg(NORMAL, "%s:%d client shutdown!", clientip.c_str(), clientport);
}
else
{
logMsg(ERROR, "read::%d:%s", errno, strerror(errno));
}
close(serviceSocket);
}
/*// version 3多线程
class ThreadInfo
{
public:
int _socketFd;
std::string _ip;
uint16_t _port;
};
*/
class TcpServer
{
private:
const static int g_backlog = 20;
/*// version 3多线程
//类内成员函数做回调函数得是静态函数 -- this
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self()); // 自动释放资源
ThreadInfo *threadinfo = static_cast<ThreadInfo *>(args);
echoService(threadinfo->_socketFd, threadinfo->_ip, threadinfo->_port);
delete threadinfo;
return nullptr;
}
*/
public:
TcpServer(uint16_t port, std::string ip = "127.0.0.1")
: _listenSocket(-1),
_port(port),
_ip(ip),
_ptrToThreadPool(singleThreadPool<Task>::getThreadPoolInstance())
{
}
void initServer()
{
// 1. 创建socket -- 进程+文件
_listenSocket = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSocket < 0)
{
logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));
exit(2);
}
logMsg(NORMAL, "show listenSocket: %d", _listenSocket); // fd = 3
// 2. bind -- 文件+网络
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
// inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
// inet_aton(_ip.c_str(), &local.sin_addr);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
local.sin_port = htons(_port);
if (bind(_listenSocket, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));
exit(3);
}
// 3. TCP面向连接 当正式通信时 要先建立连接
if (listen(_listenSocket, g_backlog) < 0)
{
logMsg(FATAL, "listen::%d:%s", errno, strerror(errno));
exit(4);
}
logMsg(NORMAL, "init server success");
}
void start()
{
// Version 2.0 主动忽略SIGCHLD信号 子进程退出时自动释放自己的资源
// signal(SIGCHLD, SIG_IGN);
_ptrToThreadPool->run();
while (true)
{
// sleep(1);
// 4. 接收连接accept
struct sockaddr_in src_sockAddr;
socklen_t len = sizeof(src_sockAddr);
int serviceSocket = accept(_listenSocket, (struct sockaddr *)&src_sockAddr, &len);
if (serviceSocket < 0)
{
logMsg(ERROR, "accept::%d:%s", errno, strerror(errno));
continue;
}
// 接收连接成功
uint16_t client_port = ntohs(src_sockAddr.sin_port);
std::string client_ip = inet_ntoa(src_sockAddr.sin_addr);
logMsg(NORMAL, "link success, serviceSocket: %d | %s : %d |\n",
serviceSocket, client_ip.c_str(), client_port);
/* // Version 1单进程回显服务器
// 一次处理一个客户端 处理完一个再处理下一个
// 多个客户端给服务端发消息 服务端只能接收到第一个客户端的信息
// 因为一旦第一个客户端连接上服务端后 服务端就就调用echoService()
// 该函数是一个死循环 他在不断地读取回显客户端发送的数据 无法再与新的客户端建立连接 除非有客户端退出
echoService(serviceSocket, client_ip, client_port);
close(serviceSocket);
*/
/*// Version 2.0多进程 信号处理子进程问题
// 创建子进程 让子进程给新的连接提供服务 子进程能打开父进程曾经打开的文件
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// 子进程是来进行提供服务的 不需要知道listenSocket
//关闭不需要的套接字 防止非法访问
close(_listenSocket);
echoService(serviceSocket, client_ip, client_port);
exit(0);
}
// 父进程: 每一次accept一个客户端 都会创建一个serviceSocket
// 如果在该层循环即将结束时 不关闭该层的serviceSocket fd资源会越来越少
// 此处关闭serviceSocket 对父进程无影响 因为欸父进程只需要监听套接字 不需要提供服务的套接字
// fd是一种有限资源【数组下标】有的内核是32/64 云服务器一般是5-10w
// 服务器死循环 任何一种导致资源浪费的情况 都是服务器编程要考虑的
close(serviceSocket); // 父进程关闭serviceSocket不会影响子进程
// waitpid();
// 多进程版初衷是让子进程为新客户端服务 此时父进程又来等待子进程的退出
// 如果不等待子进程先于父进程退出会进入僵尸状态 如果等待又违反了多进程的初衷
// ==>1.非阻塞式等待 但是较麻烦 OS会记录下子进程的pid 然后不断轮询 2.SIGCHILD
*/
/*// version2.1多进程 创建孤儿进程
pid_t id = fork();
if (id == 0)
{
// 子进程
close(_listenSocket);
if (fork() > 0)
exit(0); // 子进程退出
// 孙子进程:孤儿进程退出时 OS自动回收
// 子进程已退出 孙进程的父进程即["子进程"]先于自己退出 孙进程成为孤儿进程
echoService(serviceSocket, client_ip, client_port);
exit(0);
}
// 父进程
waitpid(id, nullptr, 0); // 子进程诞生即退出 无需阻塞等待
close(serviceSocket);
*/
/*// version 3多线程
// 在栈上创建ThreadInfo对象 把该对象的地址传给pthread_create
// threadRoutine去访问该对象 接着主线程就会进行下一次循环
// 该对象被生命周期结束 但是threadRoutine可能还未结束 -- 悬空指针
ThreadInfo *threadinfo = new ThreadInfo();
threadinfo->_socketFd = serviceSocket;
threadinfo->_ip = client_ip;
threadinfo->_port = client_port;
pthread_t tid;
// 多线程与主线程共享fd 当本次通信结束即对应的客户端退出 再关闭serviceSocket
pthread_create(&tid, nullptr, threadRoutine, threadinfo);
*/
// verison 4线程池
//此处为测试:默认知道客户端请求的是什么任务
//Task taskEcho(serviceSocket, client_ip, client_port, Task_echo);
//Task taskToggle(serviceSocket, client_ip, client_port, Task_toggle);
Task taskDictionary(serviceSocket, client_ip, client_port, Task_Dictionary);
_ptrToThreadPool->pushTask(taskDictionary);
}
}
~TcpServer() {}
private:
uint16_t _port;
std::string _ip;
int _listenSocket;
std::unique_ptr<singleThreadPool<Task>> _ptrToThreadPool;
};
线程池
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
static const char *gLevelMap[] = {
" DEBUG ",
" NORMAL",
"WARNING",
" ERROR ",
" FATAL "};
#define LOGFILE "./singleThreadPool.log"
// 日志功能: 日志等级 时间 用户自定义(日志内容/文件名/文件行) 等
void logMsg(int level, const char *format, ...)
{
#ifndef DEBUG_COMPILE // 非调试编译下 不输出DEBUG信息
if (level == DEBUG)
return;
#endif
// 1.标准日志内容
char stdBuf[1024];
// 1.1获取时间戳
time_t timestamp = time(nullptr);
if (timestamp == std::time_t(-1))
{
std::cerr << "获取时间失败" << std::endl;
exit(1);
}
// 1.2获取格式化时间
struct tm *CLK = std::localtime(×tamp); // tm *localtime(const time_t *__timer)
// 1.3将日志信息输出到日志文件
// snprintf(stdBuf, sizeof stdBuf, "[%s] [%ld] ", gLevelMap[level], timestamp);
snprintf(stdBuf, sizeof stdBuf, "[%s] [%d/%d/%d %d:%d:%d ", gLevelMap[level],
1900 + CLK->tm_year, 1 + CLK->tm_mon, CLK->tm_mday, CLK->tm_hour, CLK->tm_min, CLK->tm_sec);
// 2.用户自定义内容
va_list args;
va_start(args, format);//args指向可变参数列表起始位置
char logBuf[1024];
// int vsnprintf(char *str, size_t size, const char *format, va_list ap);
vsnprintf(logBuf, sizeof logBuf, format, args);
va_end(args); //释放资源 回复状态
//向标准输出打印
fprintf(stdout, "%s%s\n", stdBuf, logBuf);
//向文件中打印
// FILE *fp = fopen(LOGFILE, "a");
// fprintf(fp, "%s%s\n", stdBuf, logBuf);
// fclose(fp);
}
LockGuard.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t *mtx)
: _pmtx(mtx)
{
}
void lock()
{
//std::cout << "加锁中..." << std::endl;
pthread_mutex_lock(_pmtx);
}
void unlock()
{
//std::cout << "解锁中..." << std::endl;
pthread_mutex_unlock(_pmtx);
}
~Mutex()
{
}
private:
pthread_mutex_t *_pmtx;
};
// RAII风格的加锁方式
class lockGuard
{
public:
lockGuard(pthread_mutex_t *mtx)
: _mtx(mtx)
{
_mtx.lock();
}
~lockGuard()
{
_mtx.unlock();
}
private:
Mutex _mtx;
};
singleThreadPool.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
#include "Log.hpp"
const int g_threadNum = 10;
template <class T>
class singleThreadPool
{
private:
// 构造函数
// 线程池创造多个线程(把每个线程的启动例程函数 及 创建这个线程的线程池指针 传过去)
singleThreadPool(int thread_num = g_threadNum)
: _threadNum(thread_num)
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
for (int i = 1; i <= _threadNum; i++)
{
// 初始化列表区域 对象还未存在 走到函数块{}内 对象已存在 可以使用this指针
_threads.push_back(new Thread(i, startRoutine, this));
}
}
singleThreadPool(const singleThreadPool<T> ©) = delete;
const singleThreadPool<T> &operator=(const singleThreadPool<T> ©) = delete;
public:
// 考虑 多线程使用单例 的情况
static singleThreadPool<T> *getThreadPoolInstance(int threadNum = g_threadNum)
{
if (nullptr == ptrThreadPool)
{
lockGuard lockguard(&mutex_single);
if (nullptr == ptrThreadPool)
{
ptrThreadPool = new singleThreadPool<T>(threadNum);
}
}
return ptrThreadPool;
}
pthread_mutex_t *getMutex()
{
return &lock;
}
void waitCond()
{
pthread_cond_wait(&cond, &lock);
}
bool isEmpty()
{
return _taskQueue.empty();
}
void pushTask(const T &task)
{
lockGuard lockguard(&lock);
_taskQueue.push(task);
pthread_cond_signal(&cond);
}
T getTask()
{
T t = _taskQueue.front();
_taskQueue.pop();
return t;
}
void run()
{
for (auto &iter : _threads)
{
iter->start();
// std::cout << iter->name() << " 启动成功" << std::endl;
logMsg(NORMAL, "%s %s", iter->getThreadName().c_str(), "启动成功");
}
}
static void *startRoutine(void *args)
{
ThreadInfo *threadinfo = (ThreadInfo *)args;
singleThreadPool<T> *tp = (singleThreadPool<T> *)threadinfo->_ptrThreadPool;
while (true)
{
T task;
{
lockGuard lockguard(tp->getMutex());
while (tp->isEmpty())
tp->waitCond();
task = tp->getTask();
}
task(threadinfo->_threadName);
}
}
~singleThreadPool()
{
for (auto &iter : _threads)
{
iter->join();
delete iter;
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
private:
int _threadNum;
std::vector<Thread *> _threads;
std::queue<T> _taskQueue;
pthread_mutex_t lock;
pthread_cond_t cond;
static singleThreadPool<T> *ptrThreadPool;
static pthread_mutex_t mutex_single;
};
// 类内声明 类外初始化
template <typename T>
singleThreadPool<T> *singleThreadPool<T>::ptrThreadPool = nullptr;
template <typename T>
pthread_mutex_t singleThreadPool<T>::mutex_single = PTHREAD_MUTEX_INITIALIZER;
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"
// typedef std::function<void (int, const std::string &, const uint16_t &, const std::string &)> func_t;
using func_t = std::function<void(int, const std::string &, const uint16_t &, const std::string &)>;
class Task
{
public:
Task() {}
Task(int sock, const std::string ip, uint16_t port, func_t startRoutine)
: _socketFd(sock),
_ip(ip),
_port(port),
_startRoutine(startRoutine)
{
}
void operator()(const std::string &threadName)
{
_startRoutine(_socketFd, _ip, _port, threadName);
// logMsg(NORMAL, "%s running | %s | %d | %s | %s", threadName.c_str(), __FILE__, __LINE__, __DATE__, __TIME__);
}
public:
int _socketFd;
std::string _ip;
uint16_t _port;
func_t _startRoutine;
};
thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>
// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);
class ThreadInfo
{
public:
std::string _threadName;
void *_ptrThreadPool;
};
class Thread
{
public:
Thread(int index, fun_t startRoutine, void *ptrTotp)
: _startRoutine(startRoutine)
{
char nameBuf[64];
snprintf(nameBuf, sizeof nameBuf, "Thread-%d", index);
_name = nameBuf;
_tInfo._threadName = _name;
_tInfo._ptrThreadPool = ptrTotp;
}
void start()
{
pthread_create(&_tid, nullptr, _startRoutine, (void *)&_tInfo);
}
void join()
{
pthread_join(_tid, nullptr);
}
std::string getThreadName()
{
return _name;
}
~Thread()
{
}
private:
pthread_t _tid;
std::string _name;
fun_t _startRoutine;
ThreadInfo _tInfo;
};