linux【网络编程】TCP协议通信模拟实现、日志函数模拟、守护进程化、TCP协议通信流程
- 一、TCP通信简单模拟实现
- 1.1 服务端实现
- 1.1.1 接口认识
- 1.1.1.1 listen:监听socket
- 1.1.1.2 accept:获取连接
- 1.1.2 tcpServer.hpp
- 1.1.3 tcpServer.cc
- 1.2 客户端实现
- 1.2.1 接口认识
- 1.2.1.1 connect:发起连接
- 1.2.2 tcpClient.hpp
- 1.2.3 tcpClient.cc
- 1.3 优化方案
- 1.3.1 TCP网络通信----多进程版
- 1.3.2 TCP网络通信----多线程版
- 二、日志函数编写
- 三、守护进程
- 3.1 引入:为什么需要守护进程化
- 3.2 进程,守护进程化
- 四、TCP协议通信流程
- 4.1 三次握手与四次挥手感性认识
一、TCP通信简单模拟实现
Tcp通信模拟实现与Udp通信模拟实现的区别不大,一个是面向字节流,一个是面向数据报;udp协议下拿到的数据可以直接发送,tcp协议下需要创建链接,用文件描述符完成数据的读写
1.1 服务端实现
1.1.1 接口认识
1.1.1.1 listen:监听socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:创建的套接字
backlog:新连接队列的长度限制
1.1.1.2 accept:获取连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:创健的套接字,仅用于监听新链接
addr:结构体,这里说网络通信,用sockaddr_in
addrlen:结构体大小
返回值:一个新的文件描述符(套接字),这个才是和客户端通信的文件描述符
通信就用accept返回的文件描述符,面向字节流,后续都是文件操作
1.1.2 tcpServer.hpp
#pragma once
#include "logMessage.hpp"
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
namespace Server
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
using namespace std;
static const uint16_t gport = 8080;
static const uint16_t gbacklog = 5;
class tcpServer
{
public:
tcpServer(const uint16_t &port = gport)
: listen_sockfd_(-1), port_(port)
{
}
void InitServer()
{
// 1.创建socket
listen_sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd_ < 0)
{
logMessage(FATAL, "create socket error");
exit(SOCKET_ERR);
}
logMessage(NORMAL, "create 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;
if(bind(listen_sockfd_,(struct sockaddr*)&local,sizeof(local))<0)
{
logMessage(FATAL,"bind socket error");
exit(BIND_ERR);
}
logMessage(NORMAL,"bind socket success");
//3.设置socket为监听状态
if(listen(listen_sockfd_,gbacklog)<0)
{
logMessage(FATAL,"listen socket error");
exit(LISTEN_ERR);
}
logMessage(NORMAL,"listen socket success");
}
void start()
{
for(; ;)
{
//4.server获取新链接
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//sock:和客户端通信的文件描述符
int sock=accept(listen_sockfd_,(struct sockaddr*)&peer,&len);
if(sock<0)//没有获取新链接成功就执行下一次循环
{
logMessage(FATAL,"accpect sock error");
continue;
}
logMessage(NORMAL,"accept sock success");
std::cout<<"sock"<<sock<<endl;
//5.通信就用sock文件描述符,面向字节流,后续都是文件操作
/*version1*/
serverIO(sock);
close(sock);
}
}
void serverIO(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<<endl;
string outbuffer=buffer;
outbuffer+="server[echo]";
write(sock,outbuffer.c_str(),outbuffer.size());
}
else if(n==0)
{
//客户端退出
logMessage(NORMAL,"client quit ,me too!!");
break;
}
}
}
~tcpServer()
{
}
private:
int listen_sockfd_;//不负责通信,只负责监听链接,获取新链接
uint16_t port_;
};
}
1.1.3 tcpServer.cc
#include "tcpServer.hpp"
#include <memory>
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t serverport=atoi(argv[1]);
unique_ptr<tcpServer> tsvr(new tcpServer(serverport));
tsvr->InitServer();
tsvr->start();
return 0;
}
1.2 客户端实现
1.2.1 接口认识
1.2.1.1 connect:发起连接
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数与accept一样,代表的含义也一样
返回值:成功0,失败-1
1.2.2 tcpClient.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <unistd.h>
namespace Client
{
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR
};
using namespace std;
class tcpClient
{
public:
tcpClient(const string &clientip, const uint16_t &clientport)
: clientip_(clientip), clientport_(clientport), sockfd_(-1)
{
}
void InitCient()
{
// 1.创建socket
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
std::cerr << "socket create error" << endl;
exit(2);
}
}
void run()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(clientport_);
server.sin_addr.s_addr = inet_addr(clientip_.c_str());
// 发起链接
if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) != 0)
{
std::cerr << "connect create error" << endl;
}
else
{
string msg;
while (true)
{
cout << "Enter# ";
getline(cin, msg);
write(sockfd_, msg.c_str(), msg.size());
char buffer [1024];
ssize_t n=read(sockfd_,buffer,sizeof(buffer)-1);
if (n>0)
{
buffer[n]=0;
cout<<"Server处理后为# "<<buffer<<endl;
}
else
{
break;
}
}
}
}
~tcpClient() {
if(sockfd_>=0) close(sockfd_);
}
private:
int sockfd_;
uint16_t clientport_;
string clientip_;
};
}
1.2.3 tcpClient.cc
#include "tcpClient.hpp"
#include <memory>
using namespace std;
using namespace Client;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t clientport=atoi(argv[2]);
string clientip=argv[1];
unique_ptr<tcpClient> ucli(new tcpClient(clientip,clientport));
ucli->InitCient();
ucli->run();
return 0;
}
上述代码是一个单进程的版本,一个链接过来会去死循环执行serverIO,也就是说同一时间只能有一个链接过来通信,其他的链接必须阻塞等待上一个链接退出
1.3 优化方案
1.3.1 TCP网络通信----多进程版
更该tcpServer.hpp中的start函数即可,其他文件和单进程版一致
void start()
{
logMessage(NORMAL, "Thread init success");
for (;;)
{
// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客户端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 没有获取新链接成功就执行下一次循环
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version2多进程版*/
pid_t id = fork();
if (id == 0) // 子进程
{
// 关闭子进程不需要的文件描述符
close(listen_sockfd_);
// 子进程退出,父进程回收资源,孙子进程去执行任务
// 孙子进程成为孤儿进程,1号进程托管并回收其退出资源
if (fork() > 0)
exit(0);
// 孙子进程
serverIO(sock);
close(sock);//任务完成关闭文件描述符
exit(0);
}
// 细节:父进程必须关闭子进程的sock,避免一直被占用
// 这里的关闭并不是完全关闭,只是引用计数减一,并不影响孙子进程
close(sock); // 获取之后立马关闭,多次链接出现sock都一样,也可能不一样
// 父进程,阻塞等待子进程退出
pid_t ret = waitpid(id, nullptr, 0);
if (ret > 0)
{
std::cout << "waitsuccess" << ret << endl;
}
}
}
- 父进程必须关闭子进程的sock,避免一直被占用
这里的关闭并不是完全关闭,只是引用计数减一,并不影响孙子进程- waitpid这里不能单纯用非阻塞等待,当有多个连接到来的时候,并且有一个进程退出,父进程非阻塞等待,去执行accept,但是如果后续没有连接来了,就一直阻塞在accept,剩下的子进程就没法回收了
前面【信号】中曾说道子进程退出时会发送SIGCHLD信号,我们可以对其设置捕捉,忽略掉其行为,父进程就不需要阻塞等待了
void start()
{
logMessage(NORMAL, "Thread init success");
signal(SIGCHLD,SIG_IGN);//设置信号忽略行为
for (;;)
{
// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客户端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 没有获取新链接成功就执行下一次循环
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version2多进程版*/
pid_t id = fork();
if (id == 0) // 子进程
{
// 关闭子进程不需要的文件描述符
close(listen_sockfd_);
// 子进程
serverIO(sock);
close(sock);//任务完成关闭文件描述符
exit(0);
}
// 细节:父进程必须关闭子进程的sock,避免一直被占用
// 这里的关闭并不是完全关闭,只是引用计数减一,并不影响孙子进程
close(sock); // 获取之后立马关闭,多次链接出现sock都一样,也可能不一样
}
}
1.3.2 TCP网络通信----多线程版
在tcpServer类外添加ThreadData类,类内修改start函数,添加threadRoutinue函数其余不变
class tcpServer;
class ThreadData
{
public:
ThreadData(tcpServer *self, int sockfd)
: self_(self), sockfd_(sockfd)
{
}
public:
tcpServer *self_;
int sockfd_;
};
void start()
{
// 初始化线程池
ThreadPool<Task>::GetInstance()->run();
logMessage(NORMAL, "Thread init success");
// signal(SIGCHLD,SIG_IGN);
for (;;)
{
// 4.server获取新链接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// sock:和客户端通信的文件描述符
int sock = accept(listen_sockfd_, (struct sockaddr *)&peer, &len);
if (sock < 0) // 没有获取新链接成功就执行下一次循环
{
logMessage(FATAL, "accpect sock error");
continue;
}
logMessage(NORMAL, "accept sock success,get new sock:%d", sock);
/*******************************************version3:多线程版本*/
pthread_t tid;
ThreadData* td=new ThreadData(this,sock);
pthread_create(&tid,nullptr,threadRoutinue,td);
}
}
//类内调用,静态方法
static void* threadRoutinue(void* args)
{
pthread_detach(pthread_self());
ThreadData* td= static_cast<ThreadData*>(args);
td->self_->serverIO(td->sockfd_);
//在一个进程中的所有线程都可以访问到文件描述符表,属于共享资源,
//一个线程所对应的fd在使用完毕后需要进行关闭。
close(td->sockfd_);
delete td;
return nullptr;
}
多进程版,多线程版,线程池版,可参考我的Gitee
二、日志函数编写
在计算机中,日志文件是记录在操作系统或其他软件运行中发生的事件或在通信软件的不同用户之间的消息的文件。记录是保持日志的行为。在最简单的情况下,消息被写入单个日志文件。
我们借助可变参数列表来模拟实现日志函数
实现格式如:[日志等级][时间][pid][message]
#pragma once
#include <iostream>
#include <string>
#include <stdarg.h>
#include <ctime>
#include <unistd.h>
//把错误信息写到指定文件
#define LOG_NORMAL "log_nrl.txt"
#define LOG_ERR "log_err.txt"
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
//typedef char* va_list
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(DEBUG,"sss %f %d %c",2.1,6,'h')
void logMessage(int level,const char* format,...)
{
#define NUM 1024
char logprefix[NUM];
//前半部分[日志等级][时间][pid]
snprintf(logprefix,sizeof(logprefix),"[%s][%ld][%d]",to_levelstr(level),(long int)time(nullptr),getpid());
char logcontent[NUM];
va_list arg;
//初始化arg为参数列表中的第一个参数的地址
va_start(arg,format);
//后半部分,错误信息
//vsnprintf()函数的作用是将可变参数列表arg中的数据按照指定的格式format写入缓冲区logcontent中
vsnprintf(logcontent,sizeof(logcontent),format,arg);
//这里做了简化,实际上是一个等级一个日志文件
FILE* log=fopen(LOG_NORMAL,"a");
FILE* err=fopen(LOG_ERR,"a");
if(log !=nullptr && err!=nullptr)
{
if(level==DEBUG || level==NORMAL||level==WARNING)
{
fprintf(log,"%s%s\n",logprefix,logcontent);
}
if(level==ERROR || level==FATAL)
{
fprintf(err,"%s%s\n",logprefix,logcontent);
}
fclose(log);
fclose(err);
}
}
三、守护进程
3.1 引入:为什么需要守护进程化
守护进程又叫精灵进程—本质孤儿进程的一种有了守护进程,上述的服务端才能变成一个真正的服务端
1.当我们使用xsell链接远端云服务器的时候,打开的页面第一个出现的就是bash命令行,这个时候我们输入sleep 10000 | sleep 20000 |sleep 30000 &
就可以添加一个后台任务。
2.在命令行运行sleep 40000 | sleep 50000 |sleep 60000 &
后,查看进程,发现新创建的三个进程PGID一样,属于同一个组,完成一个任务,与之前的任务同属于一个会话(SID都一样)
3.通过查看SID进程发现,是bash:会话ID是以bash命名的
4.前后台任务切换
fg + 作业编号切换指定任务到前台
ctrl+z暂停任务:bash自动切换到前台
bg +作业编号指定任务stop->run
当我们进行网络通信的时候,如果服务器关机或注销了,任务就可能会被清理,导致客户端发送的消息无响应,这显然与真正的服务器不一样,我们需要把服务任务自成会话,自成进程组,不受终端设备影响-----守护进程
3.2 进程,守护进程化
#include <unistd.h>
pid_t setsid(void);//必须是非组长调用
setsid 用于在一个新的会话中启动一个进程。在运行 setsid 命令时,所启动的进程将会脱离当前的终端会话,并在一个新的会话中运行,这样它就不会受到终端会话关闭或挂起的影响,而可以持续运行。
#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(char* currPath=nullptr)
{
//1.让调用进程忽略掉异常的信号
//客户端已经退出,服务端再写会崩溃
signal(SIGPIPE,SIG_IGN);
//2.不是组长,调用setsid
if(fork()>0) exit(0);
//子进程---守护进程又叫精灵进程---本质孤儿进程
pid_t n=setsid();
assert(n!=-1);
//3.守护进程是脱离终端的,关闭或重定向以前进程默认打开的文件
// /dev/null 垃圾站,写进的内容全丢弃
int fd=open(DEV,O_RDWR);
if(fd>=0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
//012已经执行devil/null
close(fd);
}
else
{
close(0);
close(1);
close(2);
}
//4.可选:进程执行路径发生更改
//chdir:将进程的当前工作目录更改为 currpath 参数指定的目录
if(!currPath) chdir(currPath);
}
在服务端初始化完毕之后,启动之前执行daemonSelf()函数,再启动服务端,查看进程就可以得到以下信息
父进程id为1证明这个进程是一个孤儿进程,而且可以发现这个进程的PID,PGID,GID都一样,这就是自成会话,自成进程组的守护进程!!!
这个时候,即便关闭终端,只要不kill掉这个进程,他就会在一直运行响应客户端
四、TCP协议通信流程
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
4.1 三次握手与四次挥手感性认识
三次握手—建立连接:
女方对男方说想谈恋爱,男方答应并问到什么时候开始,女方说现在
四次挥手—断开连接:
比如女方对男方说离婚,男方回复离婚
同时男方反应一会觉得不行,凭什么你先对我说,我也得休了你,然后对女方说离婚,女方回复离婚