Linux学习记录——삼십일 socket编程---TCP套接字
文章目录
- TCP套接字简单通信
- 1、服务端
- 1、基本框架
- 2、获取连接
- 2、客户端
- 3、多进程
- 4、多线程
- 5、线程池
- 6、简单的日志系统
- 7、守护进程
- 8、其它
TCP套接字简单通信
本篇gitee
学习完udp套接字通信后,再来看TCP套接字。
四个文件tcp_server.hpp, tcp_server.cc,tcp_client.cc,makefile。
makefile
.PHONY: all
all:tcp_client tcp_servertcp_client:tcp_client.ccg++ -o $@ $^ -std=c++11 -lpthreadtcp_server:tcp_server.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY: clean
clean:rm -f tcp_client tcp_server
1、服务端
1、基本框架
和udp的有些一样。我们有些序列需要主机转网络,但发送的消息不需要,是因为操作系统会自动转大小端,处理交互用的消息。
tcp_server.hpp
#pragma once#include <iostream>
#include <memory>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"namespace ns_server
{static const uint16_t defaultport = 8081;class TcpServer{public:TcpServer(uint16_t port = defaultport): port_(port){}void initDerver(){//1. 创建socketsock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){std::cerr << "create socket fail" << std::endl;exit(SOCKET_ERR);}//2. 绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);local.sin_addr.s_addr = htonl(INADDR_ANY);//也可以直接写INADDR_ANYif(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){std::cerr << "bind socket fail" << std::endl;exit(BIND_ERR);}}void start(){}~TcpServer(){}private:uint16_t port_;//只要是服务器,就要有端口号int sock;};
};
err.hpp
#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR
};
tcp_server.cc
#include "tcp_server.hpp"
using namespace std;
using namespace ns_server;int main()
{unique_ptr<TcpServer> tsvr(new TcpServer());tsvr->initServer();tsvr->start();return 0;
}
tcp_client.cc
#include <iostream>int main()
{return 0;
}
接下来开始就是tcp的部分了。tcp是面向连接的,它不像udp一样可以直接接发消息,它得需要先连接再通信。
backlog先不用管,设置成一个小的数字就行。在类前设置一下
static const uint16_t defaultport = 8081;static const int backlog = 32;
//3. 监听(先让客户端连接过来,才能通信,而服务端就得一直等着连接)if(listen(sock, backlog) < 0){std::cerr << "listen socket fail" << std::endl;exit(LISTEN_ERR);//err.hpp就得加一个LISTEN.ERR}
开始写start函数。
void start(){quit_ = false;while(!quit_){//4. 客户端要连接,服务端就要先获取连接sleep(1);}}
写到这里就可以启动试试了。./tcp_server,用netstat命令来查看是否启动成功,后面的命令选项,-nltp,n把能显示成数字的显示成数字,l就是listen,t是指tcp,p是进程,打出来的内容中就有一个处于监听状态,IP地址是0.0.0.0的一个进程,显示出了它的PID,以及还有程序名字tcp_server。
2、获取连接
服务端必须处于监听状态,客户端才能来连接它。连接用的函数是accept。
addr和addrlen是客户端的数据。sockfd是一个套接字。
它的返回值实际也是一个文件描述符。accept接口,sockfd是用来监听的套接字,也就是用来连接客户端的,而它的返回值则是用来处理数据的。前面创建的sock就是这里的sockfd,为了方便,我们把它改名为listensock_。
struct sockaddr_in client;socklen_t len = sizeof(client);//4. 客户端要连接,服务端就要先获取连接int sock = accept(listensock_, (struct sockaddr*)&client, &len);
连接有可能失败,比如客户端不连接这个服务端,但这对于服务端并没有什么,它继续连接其它客户端就好,所以即使失败也继续。
void start(){quit_ = false;while(!quit_){struct sockaddr_in client;socklen_t len = sizeof(client);//4. 客户端要连接,服务端就要先获取连接int sock = accept(listensock_, (struct sockaddr*)&client, &len);if(sock < 0){std::cerr << "accept error" << std::endl;continue;}std::string clientip = inet_ntoa(client.sin_addr);uint16_t clientport = ntohs(client.sin_port);//5. 获取新连接成功,开始业务处理std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;service(sock);}}
写用来处理数据的函数service。先写一个读写操作。我们用socket创建的tcp套接字是流式套接字,访问时也是用字节流来访问的,想要读取数据,就用read系统调用来读取。read可以读文件,也可以读网络,就对应了Linux一切皆文件。
void service(int sock){char buffer[1024];while(true){ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if(s > 0) {buffer[s] = 0;std::cout << buffer << std::endl;}else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了{close(sock);std::cout << "client quit, me too" << std::endl;break;}else{close(sock);std::cerr << "read error: " << strerror(errno) << std::endl;break;}}}
这里只写了打印语句。接下来用回调来完成对数据的处理。
引入头文件functional。
加上成员变量func_t func_。
命名空间里的类的前面加上using func_t = std::function<std::string(const std::string&)>。
在读取成功后,buffer[s] = 0下一行加上std::string res = func_(buffer)。
初始化里也得初始化TcpServer(func_t func, uint16_t port = defaultport): func_(func), port_(port), quit_(true)。
然后在tcp_server.cc中写上回调函数。
#include "tcp_server.hpp"
using namespace std;
using namespace ns_server;static void usage(string proc)
{cout << "Usage:\n\t" << proc << " port\n" << endl;
}string echo(const string& message)
{return message;//简单的返回
}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(echo, port));tsvr->initServer();tsvr->start();return 0;
}
hpp文件中连接成功后调用回调函数,用的是res来接收,那么下面就不用打印buffer了,打印res就好了。然后再把res写给连接过来的客户端。
if(s > 0) {buffer[s] = 0;std::string res = func_(buffer);std::cout << res << std::endl;write(sock, res.c_str(), res.size());}
2、客户端
客户端全部都写在一个tcp_client.cc文件中。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;static void usage(string proc)
{cout << "Usage:\n\t" << proc << " port\n" << endl;
}int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}uint16_t serverport = atoi(argv[2]);string serverip = argv[1];//1. 创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){cerr << "socket error : " << strerror(errno) << endl;exit(SOCKET_ERR);}return 0;
}
客户端需要绑定吗?需要绑定,但不需要自己绑定,因为客户端来源于很多处,所以靠系统来绑定,防止端口冲突。客户端需要监听吗?服务端是监听的,客户端则不需要,客户端是连接服务端的,服务端是等待被连接的,所以客户端不需要监听listen,也不需要获取连接accept。
客户端需要做的是连接。用connect接口。
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
using namespace std;static void usage(string proc)
{cout << "Usage:\n\t" << proc << " port\n" << endl;
}int main(int argc, char* argv[])
{if(argc != 3){usage(argv[0]);exit(USAGE_ERR);}uint16_t serverport = atoi(argv[2]);string serverip = argv[1];//1. 创建套接字int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){cerr << "socket error : " << strerror(errno) << endl;exit(SOCKET_ERR);}//2. 发起连接struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_aton(serverip.c_str(), &server.sin_addr);int cnt = 5;while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0){cout << "正在尝试重连,重连次数还有: " << cnt-- << endl;if(cnt <= 0) break;}if(cnt <= 0){cerr << "连接失败..." << endl;exit(CONNECT_ERR);}//3. 连接成功char buffer[1024];while(true){string line;cout << "Enter>> ";getline(cin, line);write(sock, line.c_str(), line.size());ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if(s > 0){buffer[s] = 0;cout << "server echo >>>" << buffer << endl;}else if(s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}
3、多进程
为了让效果更明显,我们对代码做一些变更。
服务端的这部分代码,对service改一下。除了传客户端的套接字,再传进去ip和port。
void service(int sock, const std::string &clientip, const uint16_t &clientport){std::string who = clientip + "-" + std::to_string(clientport);char buffer[1024];while(true){ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if(s > 0) {buffer[s] = 0;std::string res = func_(buffer);std::cout << who << ">>>" << res << std::endl;write(sock, res.c_str(), res.size());}else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了{close(sock);std::cout << who << "quit, me too" << endl;break;}else{close(sock);std::cerr << "read error: " << strerror(errno) << std::endl;break;}}}
这样就能看出来连接的是哪个客户端,哪个客户端的消息,哪个客户端退出了。
当我们把现在所有的代码编译启动后,会发现有问题。同时开两个客户端,连接好后,只有一个在服务端那里出现了连接的消息,另一个没有,并且另一个发消息,服务端也没有打印出来,只有连接上的那个能打印消息;当把两个客户端都退出时,之前连接上的那个正常退出,而紧接着,没连接上的那个这时却打印了连接成功的消息,并且文件描述符和之前连接的那个一样,也就是说,它是在上一个文件取消连接后才去连接的,所以文件描述符是同一个数字,并且之前没有打印出来的消息这时也都打印出来了。
这就说明,目前的服务端无法处理多个客户端。我们得让服务端能多进程运行。
//5. 获取新连接成功,开始业务处理std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;//service(sock, clientip, clientport); pid_t id = fork();if(id < 0){close(sock);continue;}//子进程会继承父进程的fd,但父子不是用同一张文件描述表的,子进程会拷贝父进程的//子进程一定有sock和listensock//分工明确一下,父进程负责获取连接,子进程处理数据,所以两个进程都要close不需要的部分else if(id == 0){close(listensock_);service(sock, clientip, clientport); exit(0);}close(sock);//如果父进程不关闭,一直accept,一直往下开文件描述符,文件描述符存储在数组中,总有满的时候,就会造成文件描述符泄漏pid_t ret = waitpid(id, nullptr, 0);if(ret == id) std::cout << "wait child " << id << " succeed" << std::endl;
这里面有个明显的问题,等待默认是阻塞的,所以父进程还是在串行运行的。我们可以用非阻塞式运行,0换成WHOHANG,但是假如最后一个客户端已经连接上了,子进程在处理,父进程回去继续accept,子进程退出后,父进程还卡在那里,没办法退出了,所以不行;还可以用signal函数,子进程退出时会发出SIG_CHILD信号,那么对它捕捉并用handler处理就行,但不如直接忽略掉这个信号更方便,所以这里采用忽略。
除了忽略,还有一个办法。
else if(id == 0){close(listensock_);if(fork() > 0) exit(0);//到这里时,子进程已经退了,孙子进程在运行//子进程退,父进程就wait结束,也退了//这时候孙子进程是孤儿进程,由系统管理,所以不需要担心它的回收service(sock, clientip, clientport); exit(0);}
但fork太多,对系统要求也高,所以直接忽略就好。现在再次运行,会发现所有的客户端的文件描述符都是一个数字,这是因为有了多进程后,一个客户端连接上,子进程就会把这个客户端拿过来处理,而父进程那边给关闭了这个文件描述符,再去获取下一个连接,所以父进程给客户端分配的一直都是一个文件描述符。
4、多线程
多进程还是不够高效,把处理数据的部分换成多线程。
class TcpServer;class ThreadData{public:ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer* ts): sock(fd), clientip(ip), clientport(port), current(ts){}public:int sock;std::string clientip;uint16_t clientport;TcpServer *current;};
void start(){//signal(SIGCHLD, SIG_IGN);quit_ = false;while(!quit_){struct sockaddr_in client;socklen_t len = sizeof(client);//4. 客户端要连接,服务端就要先获取连接int sock = accept(listensock_, (struct sockaddr*)&client, &len);if(sock < 0){std::cerr << "accept error" << std::endl; continue;}std::string clientip = inet_ntoa(client.sin_addr);uint16_t clientport = ntohs(client.sin_port);//5. 获取新连接成功,开始业务处理std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;pthread_t tid;ThreadData* td = new ThreadData(sock, clientip, clientport, this);pthread_create(&tid, nullptr, threadRoutine, td);}}static void* threadRoutine(void* args){ThreadData* td = static_cast<ThreadData*>(args);td->current->service(td->sock, td->clientip, td->clientport);delete td;//service完后退出}
线程要不要关闭不要的套接字?不需要,因为多个线程共享文件描述符,所以不能关掉,关掉后服务端就不能正常运行了。这里要不要回收线程?肯定要,但如果create完后join后,join会阻塞,又会出现多进程里的问题。应当在threadRoutine函数里先detach,分离出当前线程,那么主线程就不需要管理这个分离出去的线程了,它运行完自己结束,而服务端可以继续做自己的工作。
5、线程池
现在的程序是客户端连接过来了,服务端才建立线程,为了更高效,我们可以用线程池来优化。
之前已经写过线程池了。ThreadPool_V4.hpp
#pragma once#include <iostream>
#include <memory>//智能指针的头文件
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
#include "task.hpp"
#include "LockGuard.hpp"const static int N = 5;template <class T>
class ThreadPool
{
private:ThreadPool(int num = N) : _num(num)//也可以不初始化_threads,因为我们用的是库,直接push就行{pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &tp) = delete;//去掉默认生成的拷贝构造void operator=(const ThreadPool<T> &tp) = delete;//去掉默认生成的拷贝赋值public:static ThreadPool<T> *getinstance()//这个要设置成静态的,因为如果cc文件中要调用这个静态对象的函数的话,函数也应当是静态的才行{if(nullptr == instance) //提高效率,减少加锁的次数{LockGuard lockguard(&instance_lock);//用锁类if (nullptr == instance){logMessage(Debug, "线程池单例形成");instance = new ThreadPool<T>();instance->init();instance->start();}}return instance;}pthread_mutex_t *getlock() {return &_lock; }void threadWait() {pthread_cond_wait(&_cond, &_lock); }void threadWakeup() {pthread_cond_signal(&_cond); }bool isEmpty() {return _tasks.empty(); }T popTask(){T t = _tasks.front();_tasks.pop();return t;}static void threadRoutine(void *args)//加static?类内的线程函数,要记得加static,放在静态区,因为在类内会有this指针,导致函数参数类型不对{// pthread_detach(pthread_self());ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (true){T t;{//括号里就是临界区//1、检测有没有任务,有就处理,无就等待,这里一定要加锁LockGuard lockguard(tp->getlock());//因为是静态函数,不能直接访问类内私有成员,所以init函数那里要传this指针就可以了while(tp->isEmpty()){tp->threadWait(); }t = tp->popTask();//从公共区域拿到私有区域}//测试t();}}void init(){//插入若干个线程for (int i = 0; i < _num; i++){_threads.push_back(Thread(i, threadRoutine, this));}}void start(){for (auto &t : _threads){t.run();}}void check(){for (auto& t : _threads){std::cout << t.threadname() << " running..." << std::endl;}}void pushTask(const T &t){LockGuard lockgrard(&_lock);//V2是调用系统接口,V3就是调用我们自己写的类,初始化,函数结束时自动析构,也就是释放锁_tasks.push(t);threadWakeup();}~ThreadPool(){for (auto &t : _threads){t.join();}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}
private:std::vector<Thread> _threads;//pthread_t是用库中的int _num;std::queue<T> _tasks; // 使用STL的自动扩容pthread_mutex_t _lock;pthread_cond_t _cond;//当没有任务,所有线程应当休息,挂起,所以用条件变量来控制static ThreadPool<T> *instance;//对象static pthread_mutex_t instance_lock;//静态锁
};template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;
在tcp_server.hpp里引入这个头文件。这个线程池是默认有5个线程可供使用的。task.hpp要改,不同的场景有不同的任务。
task.hpp先写一个框架
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>class Task
{
public:Task(){}Task(int sock): _sock(sock){}void operator()()//仿函数,在tcp_server.hpp中用t()来调用{}~Task(){}
private:int _sock;
};
接着看tcp_server.hpp文件。
//5. 获取新连接成功,开始业务处理std::cout << "获取新连接成功: " << sock << " from "<< listensock_ << ", " << clientip << "-" << clientport << std::endl;Task t(sock, clientip, clientport, std::bind(&TcpServer::service, this, std::placeholder::_1, std::placeholder::_2, std::placeholder::_3));//绑定类内用的方法,三个是占位符,前面三个是这个方法的参数ThreadPool<Task>::getinstance()->pushTask(t);
task.hpp中使用回调函数。
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>using cb_t = std::function<void(int, const std::string&, const uint16_t&)>;class Task
{
public:Task(){}Task(int sock, const std::string& ip, const std::uint16_t& port, cb_t cb): _sock(sock), _ip(ip), _port(port), _cb(cb){}void operator()(){_cb(_sock, _ip, _port);}~Task(){}
private:int _sock;std::string _ip;std::uint16_t _port;cb_t _cb;
};
写好后整体运行起来,会有以下的现象。程序貌似不是很快;有的客户端会连接不上,只能重连;文件描述符依次增大,如果有客户端退出,紧接着连接上的客户端就会用上退出的客户端的文件描述符;连不上的客户端等其它客户端退出一些,它们才能连上。因为service函数是一个死循环,一个线程进去执行任务后就出不来了,没有执行任务才会break,线程池也只有5个线程,这样的设计就注定如果5个线程都用上了,其它来连接的就得等着,只能处理短任务。我们也可以使用多线程的办法,在service函数中要调用函数去处理数据时在动用线程池,这样就是多线程内带着线程池。
比较简单的做法就是service变成一次的,而不是死循环,去掉while。线程池的个数也加多一些。
void service(int sock, const std::string &clientip, const uint16_t &clientport){std::string who = clientip + "-" + std::to_string(clientport);char buffer[1024];ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if(s > 0) {buffer[s] = 0;std::string res = func_(buffer);std::cout << who << ">>>" << res << std::endl;write(sock, res.c_str(), res.size());}else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了{close(sock);std::cout << who << "quit, me too" << endl;}else{close(sock);std::cerr << "read error: " << strerror(errno) << std::endl;}close(sock);}
这样的改动也只是处理简单的操作,IO数据的时候就要有更详细的做法。
6、简单的日志系统
上面的代码一直是用cout来打印消息,但实际上就写日志来记录这些信息。这里要写的日志不是完整的,而是简易版本,用来完成简单的TCP通信。创建一个log.hpp。日志中要使用v开头的几个函数。
日志是有等级的,编译器会给警告,会给报错,就是在打印日志消息。日志要处理多种类的信息。
#pragma once#include <cstdio>
#include <cstring>
#include <cstdarg>#define DEBUG 0//调试信息
#define INFO 1//正常信息
#define WARNING 2//告警,不影响运行
#define ERROR 3//一般错误
#define FATAL 4//严重错误void logMessage(int level, char* format, ...)//...就是可变参数,format是输出格式
{}
要用可变参数,需要用到几个宏
void logMessage(int level, char* format, ...)//...就是可变参数,format是输出格式
{//format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数va_list p;//char* //下面是三个宏函数int a = va_arg(p, int);//根据类型提取参数va_start(p, format);//让p指向可变参数部分的起始地址va_end(p);//把p置为空, p = NULL
}
下面写出整个功能实现。
#pragma once#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>enum
{Debug = 0,//调试信息Info,//正常信息Warning,//告警,不影响运行Error,//一般错误Fatal,//严重错误Unknown
};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 "Unknown";}
}static std::string getTime()
{time_t curr = time(nullptr);//拿到当前时间struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量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);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900;月份从0开始,要+1return buffer;
}//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()以%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{//写入到两个缓冲区中char logLeft[1024];//用来显示日志等级,时间,pidstd::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);//直接用这个接口来对format进行操作,提取信息vsnprintf(logRight, sizeof(logRight), format, p);va_end(p);//打印printf("%s%s\n", logLeft, logRight);//format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数//va_list p;//char*//下面是三个宏函数//int a = va_arg(p, int);//根据类型提取参数//va_start(p, format);//让p指向可变参数部分的起始地址//va_end(p);//把p置为空, p = NULL
}
tcp_server.hpp引入这个头文件,以及线程池头文件,都用日志来打印消息,这个在最后的代码链接中会看到。
先放上几句
//5. 获取新连接成功,开始业务处理
logMessage(Info, "获取新连接成功: %d from %d, who: %s - %d", sock, listensock_, clientip.c_str(), clientport);void service(int sock, const std::string &clientip, const uint16_t &clientport){std::string who = clientip + "-" + std::to_string(clientport);char buffer[1024];ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if(s > 0) {buffer[s] = 0;std::string res = func_(buffer);logMessage(Debug, "%s# %s", who.c_str(), res.c_str());write(sock, res.c_str(), res.size());}else if(s == 0)//和管道一样,把写端关闭,如果读到文件结尾就会返回0,而网络这里读到0,说明对方将连接关闭了{close(sock);logMessage(Info, "%s quit,me too", who.c_str());}else{close(sock);logMessage(Error, "read error, %d:%s", errno, strerror(errno));}close(sock);}
7、守护进程
如果关闭服务端,整个程序就不能继续了,但服务端应当一直存在,无论什么时候访问都行,所以我们要写守护进程。创建daemon.hpp。
通常./运行起来程序后都是前台运行,还可以在命令后加上空格和&做到后台运行,但也不能解决问题。
系统有sleep进程,我们可以sleep 10000就可以打开这个可执行文件, 然后用ps ajx | head -1 && axj | grep sleep来查看。进程有进程组,组有组号PGID。SID是会话ID,TTY是终端,有问号的就是对应的进程和终端无关,不是问号的显示的就是终端文件,这个进程打开了这个终端,并向这个终端文件放入内容。用户使用命令ls,pwd这样的时候,就是进程运行时在用户这里打开了终端文件,向这个文件输入内容。
打开的几个程序,如果以管道连接起来,那么PGID,会话id(SID)和终端文件都是一样的,都是第一个进程的,第一个进程也是组长,不过如果我们分为几个前台工作,几个后台工作,假如都是sleep进程,那么后台和前台不同的是PGID,但都是一个会话,打开一个终端文件。sleep的会话id其实就是bash。
会话包含多个线程组,一个线程组包含多个线程;会话关联一个终端文件;进程之间有组关系,组长都是多个进程中的第一个。
当用户登录云服务器时,登录成功会分配一个命令行提示符,也就是用户输入命令时前面的[…@…]这部分,这本质也是一个进程,也属于一个进程组,组内只有它自己,也属于一个会话,这个会话是由bash创建的,这个会话以它来起名,之后所有的用户建立的进程都属于这个会话,只是进程组不同。进程组在会话中,一个会话里,操作系统就给用户创建多个进程组。每次登录都会创建一个新的会话。
为什么要有进程组?jobs命令可以查看当前会话中所有的后台程序,每个后台程序最前面都有1个数字,从1开始,只要增加一个程序就数字就加1,每次创建的一个程序,自成一个进程组,所以PGID不同。前面的数字编号,叫做任务编号,用命令fg 任务编号就可以把这个程序放到前台(后台的任务编号不变),用Ctrl + Z就会让这个程序停止,就会自动回到后台,用bg 任务编号会让这个程序再次运行起来。所以进程组创建是为了完成任务的,一个任务可以由多个进程完成,也可以由一个进程完成。所以用户用命令启动的一个进程,其实就是在启动一个任务。
进程组有前台和后台任务,如果把后台任务提到前台,老的前台任务就无法运行,前台任务只能有一个在运行,比如提到前台后,输入命令就不起作用了,所以用户在用命令行启动一个进程时,bash无法运行。登录云服务器时就是在创建一个会话,会话里有bash任务,启动进程时就是在当前会话中创建新的前台任务,而退出则是销毁会话,会影响会话内部的所有任务。销毁会话就是注销,通常的网络服务器,为了不受到用户登录注销的影响,会以守护进程的方式运行。既然创建的进程都会在一开始登录时创建的会话里,注销时也会注销这个会话,那就让被守护的进程放入另外一个会话,这样注销就不会受影响了,这就是守护进程的做法。
需要用到setsid接口
创建一个会话,设置进程组ID,谁调用这个接口,谁就是组长。返回新会话的ID,也就是这个进程的ID,失败返回-1,错误码被设置。
如何创建守护进程?
核心是setsid接口,但不止这点。要想调用这个接口,不能是组长调用,这样就得保证调用者不是组长。守护进程要忽略异常信号,并对文件描述符012做特殊处理,改变工作路径。进程的工作路径默认为当前路径,但守护进程不想这样,它会放在根目录下,不属于某个用户目录。更改路径这个操作用daemon这个接口,两个参数分别表示要不要更改路径,要不要关闭012。
不过一般是自己来更改路径,不用这个接口。外部的调用逻辑是这样的,也就是tcp_server.cc中
tsvr->initServer();//将服务器守护进程化Daemon();tsvr->start();
err.hpp中加上SETSID_ERR这个错误
#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,SETSID_ERR
};
如果daemon.hpp这样写
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include "log.hpp"
#include "err.hpp" void Daemon()
{pid_t ret = setsid();if((int)ret == -1){lodMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));exit(SETSID_ERR);}
}
./运行起服务端肯定出错,因为新创建的这个进程自成一组,它是组长,就不行,所以得先让它不是组长,只要不是第一个进程就好了。
void Daemon()
{if(fork() > 0) exit(0);//下面的就是子进程了pid_t ret = setid();if((int)ret == -1){lodMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));exit(SETSID_ERR);}
}
以及还需要忽略异常信号等其它。
#pragma once#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 = setid();if((int)ret == -1){logMessage(Fatal, "daemon error, code: %d, string: %s", errno, strerror(errno));exit(SETSID_ERR);}//4. 可选: 更改守护进程的工作路径//因为我们自定义的一些头文件,这里就不改路径了//chdir("/")//更改为根目录//5. 处理012问题//Linux中有个/dev/null文件,任何向里面输入的内容都会被抛弃,不会被提取内容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);
}
在tcp_server.cc文件中,守护进程后还会有start函数,这个函数里可能有cout,cin等,守护进程后使用这些就会出错,所以要把错误重定向到/dev/null中。守护进程本质是孤儿进程的一种。这时候再次启动服务端就可以了。启动后用命令
ps ajx | head -1 && ps -axj | grep tcp_server
grep后面的就是进程名字。可以发现TTY是?,SID是一个新的组。关闭云服务器后,这个服务端依然可以提供服务。用jobs查看不到。
想要关闭服务端,kill -9 SID就可以。但是还有一个问题,把标准输入输出错误都重定向到/dev/null了,那么日志打印的消息程序员也就看不到了,就不知道服务器会出什么问题了,所以我们还得更改一下log.hpp,让它把消息打印到当前路径的一个文件中。
#pragma once#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cstdarg>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>const std::string filename0 = "log/tcpserver.log.Debug";
const std::string filename1 = "log/tcpserver.log.Info";
const std::string filename2 = "log/tcpserver.log.Warning";
const std::string filename3 = "log/tcpserver.log.Error";
const std::string filename4 = "log/tcpserver.log.Fatal";
const std::string filename5 = "log/tcpserver.log.Unknown";enum
{Debug = 0,//调试信息Info,//正常信息Warning,//告警,不影响运行Error,//一般错误Fatal,//严重错误Unknown
};static std::string toLevelString(int level, std::string& filename)
{switch(level){case Debug:filename = filename0;return "Debug";case Info:filename = filename1;return "Info";case Warning:filename = filename2;return "Warning";case Error:filename = filename3;return "Error";case Fatal:filename = filename4;return "Fatal";default:filename = filename5;return "Unknown";}
}static std::string getTime()
{time_t curr = time(nullptr);//拿到当前时间struct tm *tmp = localtime(&curr);//这个结构体有对于时间单位的int变量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);//这些tm_的变量就是结构体中自带的,tm_year是从1900年开始算的,所以+1900return buffer;
}//日志格式: 日志等级 时间 pid 消息体
//logMessage(DEBUG, "hello: %d, %s", 12, s.c_str()); 12以%d形式打印, s.c_str()以%s形式打印
void logMessage(int level, const char* format, ...)//...就是可变参数,format是输出格式
{//写入到两个缓冲区中char logLeft[1024];//用来显示日志等级,时间,pidstd::string filename;std::string level_string = toLevelString(level, filename);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);//直接用这个接口来对format进行操作,提取信息vsnprintf(logRight, sizeof(logRight), format, p);va_end(p);//打印printf("%s%s\n", logLeft, logRight);//format是一个字符串,里面有格式,比如%d, %c,通过这个就可以用arg来提取参数//保存到文件中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置为空, p = NULL
}
8、其它
man inet_addr会看到很多接口,inet_ntoa是把四字节IP转换为字符串,但它是C接口,返回类型是char*,也就是说返回了指针,返回了地址,而字符串是系统在内存中申请了一块空间来存储,这个位置不需要我们手动释放,但频繁调用,后面的会覆盖前面的地址,也就是说这个接口不是线程安全的,所以在多线程场景中会出问题。不过到现在为止,应当是加上了线程安全,可以用这段代码测试
#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;while (1){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;while (1) {char* ptr = inet_ntoa(addr->sin_addr);printf("addr2: %s\n", ptr);}return NULL;
}int main()
{pthread_t tid1 = 0;struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr = 0;addr2.sin_addr.s_addr = 0xffffffff;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;
}
四字节ip转为字符串还可以用inet_ntop,把IP地址转为字符串,把二进制转为文本,src参数就是IP地址,dst是一个char类型的缓冲区,定义一个buffer[]来保存转化好的字符串。
TCP协议中,服务器监听后,客户端就可以连接了,客户端的connect实际上是在发送报文,操作系统底层进行三次握手处理连接过程,处理完后服务端的accept接口就把这个创建好的连接给用户使用;close时是进行四次挥手来断开连接。建立和断开连接是用户让系统做的。建立时,客户端完成两次操作,服务端完成一次;断开时,双方都close,一次close对应两次操作。
结束。
相关文章:
Linux学习记录——삼십일 socket编程---TCP套接字
文章目录 TCP套接字简单通信1、服务端1、基本框架2、获取连接 2、客户端3、多进程4、多线程5、线程池6、简单的日志系统7、守护进程8、其它 TCP套接字简单通信 本篇gitee 学习完udp套接字通信后,再来看TCP套接字。 四个文件tcp_server.hpp, tcp_serve…...
【学习笔记】深度学习分布式系统
深度学习分布式系统 前言1. 数据并行:参数服务器2. 流水线并行:GPipe3. 张量并行:Megatron LM4. 切片并行:ZeRO5. 异步分布式:PATHWAYS总结参考链接 前言 最近跟着李沐老师的视频学习了深度学习分布式系统的发展。这里…...
【数据结构】树、二叉树的概念和二叉树的顺序结构及实现
目录 前言:一、树的概念及结构1.树的概念2.树的相关概念3.树的存储4.树在实际中的运用 二、二叉树概念及结构1.概念2.特殊的二叉树(1)满二叉树(2)完全二叉树 3.二叉树的性质4.二叉树的存储(1)顺序存储(2)链式存储 三、…...
rust学习-string
介绍 A UTF-8–encoded, growable string(可增长字符串). 拥有string内容的所有权 A String is made up of three components: a pointer to some bytes, a length, and a capacity. The length is the number of bytes currently stored in the buffer pub fn as_bytes(&…...
No167.精选前端面试题,享受每天的挑战和学习
🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…...
【python】pycharm导入anaconda环境
参考 Pycharm导入anaconda环境的教程图解 - 知乎 (zhihu.com)...
【数据结构】逻辑结构与物理结构
🦄个人主页:修修修也 🎏所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 🌳逻辑结构 1.集合结构 2.线性结构 3.树形结构 4.图形结构或网状结构 🌳物理结构 1.顺序存储结构 2.链式存储结构 结语 根据视点的不同,我…...
HTML5高级部分
目录 一、拖拽API1.1 拖拽元素1.2 监听事件1.3 dataTransfer传递数据 二、媒体API2.1 常用监听事件2.2 常用API 三、画布API3.1 canvas 标签3.2 创建canvas对象3.3 常用API 四、地理API4.1 方法 一、拖拽API 1.1 拖拽元素 页面中设置了draggable"true"的元素可以进…...
浏览器输入 URL 并回车发生了什么
本文节选自我的博客:浏览器输入 URL 并回车发生了什么 💖 作者简介:大家好,我是MilesChen,偏前端的全栈开发者。📝 CSDN主页:爱吃糖的猫🔥📣 我的博客:爱吃糖…...
asp.net core mvc 文件上传,下载,预览
//文件上传用到了IformFile接口 1.1文件上传视图 <form action"/stu/upload" method"post" enctype"multipart/form-data"><input type"file" name"img" /><input type"submit" value"上传&…...
Axios有哪些常用的方法?
Axios是一个常用的JavaScript库,用于进行HTTP请求。它提供了一组简洁而强大的方法来发送各种类型的请求,并处理响应数据。以下是Axios中一些常用的方法及其格式: GET请求: axios.get(url[, config]).then(response > {// 请求…...
PL/SQL+cpolar公网访问内网Oracle数据库
文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle,是甲骨文公司的一款关系…...
stable diffusion和gpt4-free快速运行
这是一个快速搭建环境并运行的教程 stable diffusion快速运行gpt快速运行 包含已经搭建好的环境和指令,代码等运行所需。安装好系统必备anaconda、conda即可运行。 stable diffusion快速运行 github: AUTOMATIC1111/稳定扩散网络UI:稳定扩散网页用户界…...
分享三个国内可用的免费GPT-AI网站
AIchatOS国内的不需要梯子 AItianhu同上 国内百度的文心一言一样非常优秀...
使用SDKMAN在Linux系统上安装JDK
本文使用的Linux发行版为Rocky Linux 9.2,可以当做CentOS的平替产品。 SDKMAN是一个sdk包管理工具,通过自带的命令可以快速切换软件环境, 官网地址:https://sdkman.io/。 1、安装sdkman: # curl -s "https://ge…...
MySQL(8) 优化、MySQL8、常用命令
一、MySQL优化 从上图可以看出SQL及索引的优化效果是最好的,而且成本最低,所以工作中我们要在这块花更多时间。 服务端参数配置; max_connections3000 连接的创建和销毁都需要系统资源,比如内存、文件句柄,业务说的支持…...
前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— Web APIs(三)
思维导图 全选案例 大按钮控制小按钮 小按钮控制大按钮 css伪类选择器checked <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><…...
嵌入式汇编大合集
嵌入式汇编 内联汇编的基本格式: asm volatile( /* volatile : 可选,禁止编译器对汇编代码进行优化 */"汇编指令" /* 汇编指令间使用\n分隔 */:"=限制符"(输出参数):"限制符"(输入参数):保留列表 )共四个部分:汇编语句,输出部分,输入部分…...
C#WPF框架MvvMLight应用实例
本文实例演示C#WPF框架MvvMLight应用实例。 目录 一、MVVM概述 二、MVVMLight概述 三、使用MVMLight框架 一、MVVM概述 MVVM概述MVVM是Model-View-ViewModel的简写,主要目的是为了解耦视图(View)和模型(Model)。...
【JVM】双亲委派模型
双亲委派模型 1. 什么是双亲委派模型2. 双亲委派模型的优点 1. 什么是双亲委派模型 提到 类加载 机制,不得不提的一个概念就是“双亲委派模型”。 双亲委派模型指的就是 JVM 中的类加载器如何根据类的全限定名找到 .class 文件的过程 类加载器: JVM 里面专门提供…...
多叉树+图实现简单业务流程
文章目录 场景整体架构流程业务界面技术细节小结 场景 这次遇到一个需求,大致就是任务组织成方案,方案组织成预案,预案可裁剪调整.预案关联事件等级配置,告警触发预案产生事件.然后任务执行是有先后的,也就是有流程概念. 整体架构流程 方案管理、预案管理构成任务流程的基础条…...
Word | 简单可操作的快捷公式编号、右对齐和引用方法
1. 问题描述 在理工科论文的写作中,涉及到大量的公式输入,我们希望能够按照章节为公式进行编号,并且实现公式居中,编号右对齐的效果。网上有各种各样的方法来实现,操作繁琐和简单的混在一起,让没有接触过公…...
leetCode 123.买卖股票的最佳时机 III 动态规划 + 状态压缩
123. 买卖股票的最佳时机 III - 力扣(LeetCode) 给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 注意:你不能同时参与多笔交易࿰…...
JavaScript计算两个时间相差多少个小时的封装函数
js中计算两个时间相差小时数 在JavaScript中,你可以使用Date对象来处理日期和时间。下面是一个函数,它接受两个时间字符串作为参数,并返回两者之间的时间差(以小时为单位): function calculateHours(time…...
Qt 画自定义饼图统计的例子
先给出结果图,这个例子是将各种事件分类然后统计的其比例,然后画饼图显示出来 这个是我仿照官方给的例子,让后自己理解后,修改的,要生成饼图,需要QT的 charts 支持,安装QT 没有选择这个的&#…...
【数据结构】链表与LinkedList
作者主页:paper jie 的博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文录入于《JAVA数据结构》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精…...
Flink RoaringBitmap去重
1、RoaringBitmap的依赖 <!-- 去重大哥--> <dependency><groupId>org.roaringbitmap</groupId><artifactId>RoaringBitmap</artifactId><version>0.9.21</version> </dependency> 2、Demo去重 package com.gwm.driver…...
Elasticsearch—(MacOs)
1⃣️环境准备 准备 Java 环境:终端输入 java -version 命令来确认版本是否符合 Elasticsearch 要求下载并解压 Elasticsearch:前往(https://www.elastic.co/downloads/elasticsearch)选择适合你的 Mac 系统的 Elasticsearch 版本…...
插入排序与希尔排序
个人主页:Lei宝啊 愿所有美好如期而遇 前言: 这两个排序在思路上有些相似,所以有人觉得插入排序和希尔排序差别不大,事实上,他们之间的差别不小,插入排序只是希尔排序的最后一步。 目录 前言:…...
C# OpenCvSharp 基于直线检测的文本图像倾斜校正
效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp;namespace OpenCvSharp_基于直线检测的文…...
杭州做公司网站的公司/搜索引擎是指什么
点击上方“蓝色字”可关注我们!暴走时评:美国加密货币交易所Coinbase证实,它已于5月13日向纽约用户推出了XRP交易。 在比特币(BTC)持续的牛市中,XRP在周二表现最佳,XRP /美元在24小时内创下了22…...
wordpress本地播放器/成都网站建设seo
欢迎访问我的个人博客:http://www.xiaolongwu.cn 前言 今天我们来做一个有趣的测试,那就是我们在某个范围之间取随机数时,每项被随机到的概率是否相等。 随机方法 我们都知道Math.random()的结果是[0, 1)之间的小数,结果包括0但是…...
什么网站做推广农产品比较好/推广工具有哪些
学习了这么多年,读过不少书,学习了不少知识。 我们 读书,读的很肤浅,很少能静下心里,去读一些获取知识的书。 现在读书仿佛也很“快餐”,读一本书,读一个大概,获取自己所需要的知识即…...
wordpress网站如何app/百度网盘搜索引擎
文章目录大数据人工智能常见技术框架和算法的知识详细概要HadoopSparkHiveStromHbaseSolrElasticsearchLDA潜在狄利克雷分布模型MinHash聚类Kmeans聚类Canopy聚类贝叶斯分类算法总结大数据人工智能常见技术框架和算法的知识详细概要 Hadoop Hadoop是一个由Apache基金会所开发…...
有没有兼职做设计的网站吗/网站seo优化是什么意思
VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。 DTO(Data Transfer Object):数据传输对象,这个概…...
学校网站建设重要性/seo学习网站
早晨起床时间:6:10 晚上休息时间:23:31 全天处理事件:1.收尾电子时钟软件程序版本2.0。 处事经验总结:暂无。 人生感悟:性格决定行为。 其它:这段时间还是感觉比较迷茫,在社会上经历一段时间后&…...