一、本章重点
1. tcp服务器实现思路,进一步了解和总结相关的接口
2. 了解日志和守护进程
二、tcp服务器核心思路
tcp版的服务器与udp的不同在于,udp是面向数据报传输数据,在数据传输中不需要建立与客户端的链接,直接用recvfrom和sendto这两个接口进行消息的收发,而tcp版的服务器则是面向字节流的,需要与客户端建立连接
1. 创建监听套接字listen_sock
这个监听套接字是用于监听的,监听就可以看做为等待客户端连接,监听套接字只负责与客户端建立起连接,然后剩下的任务交给其他的套接字去执行
// 1. 创建ListenSock
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
2. 创建好struct sockaddr_in ,并将其与套接字bind
这里和udp是一样的步骤
// 2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(BIND_ERR);
}
3. 监听 -- listen
//?
if (listen(_listensock, backlog) < 0)
{
std::cerr << "listen socket error" << std::endl;
exit(LISTEN_ERR);
}
4. 获取连接 -- accept
获取连接其实就是获取客户端的连接,它会返回一个套接字,后续的业务处理则是根据这个套接字去实现和完成的
// 1. 获取连接 -- accept
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listensock, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cerr << "sock error" << std::endl;
exit(SOCKET_ERR);
}
5. 开展业务处理.
具体开展的业务处理要采用多进程或者多线程的方式,主线程负责监听连接,而其他线程负责为客户端提供具体服务。tcp的读写可以直接使用以往的文件相关的读写函数,如read、write等等
// 3. 开展业务处理 -- service
// 测试
std::cout << "连接成功:" << sock << " from " << _listensock << "," << client_ip << " - " << client_port << std::endl;
// service(sock,client_ip,client_port);
// 业务处理需要和监听同时进行,因为这里的业务处理是阻塞等待客户端的
// 3.1 多进程方案 -- 如何处理进程等待的阻塞问题?
// pid_t id = fork();
// if (id < 0)
// {
// close(sock);
// continue;
// ;
// }
// else if (id == 0) // child
// {
// close(_listensock);
// service(sock, client_ip, client_port);
// exit(0);
// }
// close(sock); -- 线程方案中不能关闭这个
// 进程等待 -- waitpid
// 方案一:信号忽略 -- signal(SIGCHLD, SIG_IGN);(推荐)
// 方案二:信号捕捉 -- signal(SIGCHLD, handler);(麻烦,不太推荐)
// 方案三:轮询等待 -- WNOHANG(不太推荐)
// 方案四:孤儿进程 -- if(fork() > 0) exit(0);(不太推荐,但思路很优秀,但是会对系统有负担)
// pid_t ret = waitpid(id, nullptr, 0);
// if (ret == id)
// std::cout << "wait child: " << id << " success" << std::endl;
// 3.2 多线程方案 -- 线程池优化
// pthread_t tid;
// ThreadData* td = new ThreadData(sock,client_ip,client_port,this);
// pthread_create(&tid,nullptr,threadRoutine,td);
Task t(sock, client_ip, client_port, std::bind(&TcpServer::service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));//?
ThreadPool<Task>::getinstance()->pushTask(t);
三、Socket编程相关接口整理
1. 网络编程相关的头文件
#include<sys/types.h> // 包含很多系统数据类型
#include<sys/socket.h> // 包含了基本的 socket 函数和数据结构定义。
#include<netinet/in.h> // 定义了 Internet 地址族相关的结构和函数。
#include<arpa/inet.h> // 提供 IP 地址转换函数。
2. socket -- 创建套接字函数
int socket(int domain, int type, int protocol);
3. struct sockaddr_in
4. bind 绑定套接字和struct sockaddr的函数接口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
5. recvfrom -- 从套接字中获取信息
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
6. sendto -- 向套接字中写入信息
int sendto(int sockfd, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);
7. 网络字节序接口
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//将源主机中32位长整形转换成网络字节序要求的大端格式
uint16_t htons(uint16_t hostshort);//将源主机中16位短整形转换成网络字节序要求的大端格式
uint32_t ntohl(uint32_t netlong);//将网络中32位长整形(大端)转换成当前主机的格式
uint16_t ntohs(uint16_t netshort);//将网络中16位短整形(大端)转换成当前主机的格式
8. listen 监听
int listen(int sockfd, int backlog);
它用于将一个套接字标记为被动套接字,也就是将其设置为用于监听连接请求的状态。这个套接字通常是由socket
函数创建并经过bind
函数绑定到一个本地地址和端口之后使用。
9. accept 连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
它用于从处于监听状态的套接字的连接请求队列中取出一个连接请求,并创建一个新的套接字来与客户端进行通信。这个新的套接字与原来监听的套接字不同,它是专门用于和客户端进行数据交互的。
四、日志
往往在项目工程中,我们会有需要不同的调试信息,以及可能一些需要标记或者记录的信息,例如用户连接成功的信息,用户的消息记录等等,这些我们之前都是直接打印在屏幕上的,但实际在工程项目中,我们都需要将这些信息打印到文件中进行管理,这里我们简单实现一下应该粗糙的日志。
1. 可变参数
在写日志代码前,还需要对可变参数有一些了解,像C语言中的printf的实现就是用可变参数的方式实现,那么我们要如何使用呢?
以上是对于可变参数的预备知识,我们要用可变参数的是为了让打印日志信息函数中,可以让外部传入的日志信息可以是多样的,像printf一样使用,所以可以用到一个函数帮助我们
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
2. 参考代码
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
// 日志是有日志等级的
const std::string filename = "log/tcpserver.log";
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal,
Uknown
};
static std::string toLevelString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Uknown";
}
}
static std::string getTime()
{
time_t curr = time(nullptr);
struct tm *tmp = localtime(&curr);
char buffer[128];
snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d", tmp->tm_year + 1900, tmp->tm_mon+1, tmp->tm_mday,
tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
return buffer;
}
// 日志格式: 日志等级 时间 pid 消息体
// logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); // DEBUG hello:12, world
void logMessage(int level, const char *format, ...)
{
char logLeft[1024];
std::string level_string = toLevelString(level);
std::string curr_time = getTime();
snprintf(logLeft, sizeof(logLeft), "[%s] [%s] [%d] ", level_string.c_str(), curr_time.c_str(), getpid());
char logRight[1024];
va_list p;
va_start(p, format);
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);
// 打印
// printf("%s%s\n", logLeft, logRight);
// 保存到文件中
FILE *fp = fopen(filename.c_str(), "a");
if(fp == nullptr)return;
fprintf(fp,"%s%s\n", logLeft, logRight);
fflush(fp); //可写也可以不写
fclose(fp);
// 预备
// va_list p; // char *
// int a = va_arg(p, int); // 根据类型提取参数
// va_start(p, format); //p指向可变参数部分的起始地址
// va_end(p); // p = NULL;
}
五、守护进程
1. 概念
要理解守护进程,我们先需要知道一些关于Liunx系统的概念。
首先要理解关于进程、进程组、会话,这三个概念
我们现在使用的Xshell去登录远端的Linux服务器,每次登录其实是在整个Linux系统上启动了一个会话,而bash进程作为首个登入的进程,同时也是对应进程组的组长
而守护进程就是,我们要将原先我们在该会话中写的进程,将其独立出去作为一个单独的会话层面的进程,让它在后台运行,一般的服务器都是如此,这样就不会因为我们会话关闭而服务器挂掉。
守护进程要实现还有几个条件:
2. 参考代码
#pragma once
// 1. setsid();
// 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
// 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "err.hpp"
//守护进程的本质:是孤儿进程的一种!
void Daemon()
{
// 1. 忽略信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 让自己不要成为组长
if (fork() > 0)
exit(0);
// 3. 新建会话,自己成为会话的话首进程
pid_t ret = setsid();
if ((int)ret == -1)
{
logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
exit(SETSID_ERR);
}
// 4. 可选:可以更改守护进程的工作路径
// chdir("/")
// 5. 处理后续的对于0,1,2的问题
int fd = open("/dev/null", O_RDWR);
if (fd < 0)
{
logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
exit(OPEN_ERR);
}
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
总结
本篇文章主要是整理了Socket编程常见的接口,还有对tcp服务器相关的概念知识点