当前位置: 首页 > news >正文

第二部分(下):套接字

目录

1、TCP

1.1、TCP的接口

1.2、TCP的应用

2、会话

2.1、前台与后台进程

2.2、进程组

2.3、会话的概念

2.4、会话关闭后的进程组

3、守护进程

3.1、守护进程化

3.2、应用

4、TCP通信流程


1、TCP

1.1、TCP的接口

例如:使用socket函数创建一个套接字。

 #include <sys/socket.h>int socket(int domain, int type, int protocol);

如果成功的话,就像open()一样返回一个文件描述符,如果socket()调用出错则返回-1;对于IPv4, family参数指定为AF_INET;对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议;protocol参数的介绍从略,指定为0即可。

例如:使用bind函数将套接字与IP和端口进行绑定。

#include <sys/socket.h>int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()成功返回0,失败返回-1。bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号,addelen则是指addr所指向空间的大小。

注意:对于TCP来讲,公网IP也是无法被云服务器绑定的,但是可以绑定127.0.0.1这样的IP地址。

注意:服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用bind绑定一个固定的网络地址和端口号。客户端则不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,否则如果在同一台机器上启动多个客户端,就可能会出现端口号被占用导致不能正确建立连接,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

例如:inet_aton函数将点分十进制表示的IPv4地址转换为无符号整数形式的网络字节序列。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>int inet_aton(const char *cp, struct in_addr *inp);

其中cp指向点分十进制IPv4地址字符串的指针,inp指向struct in_addr结构体的指针,用于存储转换后的网络字节序整数,若为 NULL,函数仅检查地址格式是否有效,不存储结果。成功转换返回1,地址格式无效返回0。

注意:该函数不会返回 -1,错误仅通过返回0表示。

例如:listen函数的主要功能是将一个主动连接的socket转换为被动监听socket,用于接受客户端的连接请求。

#include <sys/socket.h>int listen(int sockfd, int backlog);

listen声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里的backlog设置不会太大。成功返回0,失败返回-1。

例如:使用accept函数阻塞等待并接受客户端的连接。

#include <sys/socket.h>int accept(int sockfd, struct sockaddr *_Nullable restrict addr,socklen_t *_Nullable restrict addrlen);

其中,sockfd是socket函数的返回值,三次握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来,addr是一个传出参数,accept()返回时传出客户端的地址和端口号,如果给addr参数传NULL,表示不关心客户端的地址,addrlen参数是一个传入传出参数,传入的是addr所指向空间的大小。

函数成功返回一个新的socket描述符,用于与对应的客户端通信(应用程序可以像读写文件一样用read或write在网络上收发数据),失败返回 -1,并设置errno表示错误。

例如:客户端通过connect函数可以向服务器发起连接请求。

#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址,connect()成功返回0,出错返回-1。

例如:使用inet_pton函数用于将点分十进制的字符串形式的IP地址转换为无符号整数形式的网络字节序列。

#include <arpa/inet.h>int inet_pton(int af, const char *restrict src, void *restrict dst);

其中,af是指地址族,指定转换的 IP 地址类型,常见取值:AF_INET,表示 IPv4 地址、AF_INET6,表示 IPv6 地址;src指向待转换的字符串形式的IP地址;dst指向存储转换结果的缓冲区。成功返回1,返回0表示src指向的字符串不是有效的IP地址,返回-1表示发生错误,此时errno 会被设置。

注意:在套接字中,对于通信的内容,所使用的接口会默认进行主机序列转网络序列,只不过IP地址和端口号特殊一些,需要我们自己进行转换。

下面介绍一个命令telnet,在Linux中,telnet 是一个用于通过TCP/IP协议与远程主机建立连接并进行文本交互的命令行工具。它最初用于远程登录(类似ssh),但由于传输数据未加密,安全性较低,现在更多用于测试网络服务端口的连通性(如检查服务器某个端口是否开放、能否建立连接)。

telnet IP地址 端口号

使用该命令后默认是进入到了数据传输模式,此时输入内容,按enter键后,即可发送消息;按ctrl 与 ] 可以进入命令模式,此模式下可以输入quit可以退出链接,也可以使用help来查看一下命令模式中的命令。

注:另外Windows中也有这个命令,不过要想使用这个命令是需要下载的。

1.2、TCP的应用

例如:进行简单的进行客户端和服务器之间的通信。

TcpServer.hpp:

#pragma once
#include "Log.hpp"#include <sys/socket.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>#include <iostream>
#include <cstdlib>
#include <cstring>
#include <string>Log lg;
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;enum{UsageError = 1,SocketError,BindError,ListenError
};class TcpServer; // 声明class ThreadData // 线程数据
{
public:ThreadData(int fd, const std::string& ip, const uint16_t p, TcpServer* t):sockfd(fd), clientip(ip), clientport(p), tsvr(t){}public:int sockfd; // 描述符std::string clientip; // 客户端IP地址uint16_t clientport; // 客户端端口号TcpServer* tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t& port, const std::string& ip = defaultip):listensock_(defaultfd), port_(port), ip_(ip){}void InitServer() // 初始化服务器{listensock_ = socket(AF_INET, SOCK_STREAM, 0);if(listensock_ < 0){lg(Fatal, "create socket errno: %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket sucess, listensock_: %d", listensock_);struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);inet_aton(ip_.c_str(), &(local.sin_addr));//local.sin_addr.s_addr = INADDR_ANY;if(bind(listensock_, (const struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket sucess, listensock_: %d", listensock_);if(listen(listensock_, backlog) < 0)  // TCP是面向连接的,服务器要处于一种一直在等待连接的状态{lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "listen socket sucess, listensock_: %d", listensock_);}static void* Routine(void* args) // 线程执行函数{pthread_detach(pthread_self());ThreadData* td =  static_cast<ThreadData*>(args);td->tsvr->Service(td->sockfd, td->clientip, td->clientport);delete td;return nullptr;}void Start()  // 启动服务器{lg(Info, "tcpServer running...");for(;;){// 1、获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr*)&client, &len);  // 等待客户端的连接if(sockfd < 0){lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);// 2、根据新连接进行通信/* * 单进程版本:启动一个客户端后,再启动一个客户端,尝试连接服务器, 发现第二个客户端,不能正确的和服务器进行通信,* 原因是因为我们accecpt了一个请求之后, 就在一直while循环尝试read, 没有继续调用到accecpt, 导致不能接受新的请求,* 当前的这个TCP,只能处理一个连接,这是不科学的。*/// Service(sockfd, clientip, clientport); // close(sockfd);/* * 多进程版本:虽然可以处理多个客户端但是创建进程的代价比较高。*/// pid_t id = fork();// if(id == 0)// {//     // 子进程//     close(listensock_); // 把不需要的文件描述符给关掉//     if(fork() > 0) exit(0);//     Service(sockfd, clientip, clientport); // 孙子进程,会被系统自动回收//     close(sockfd);//     exit(0);// }// // 父进程// close(sockfd); // 把不需要的文件描述符给关掉// pid_t rid = waitpid(id, nullptr, 0);// (void)rid;/* * 多线程版本:虽然创建线程消耗的资源比创建进程小很多,但每次请求来时才开始创建线程还是慢了一些,线程的数量也是有上限的,此外提供长服务也是不合理的。*/ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);}}void Service(int sockfd, const std::string& clientip, const uint16_t& clientport) // 服务{char buffer[4096];while(true){ssize_t n = read(sockfd, buffer, sizeof(buffer)); // 读if(n > 0) {buffer[n] = 0;std::cout << "client say# " << buffer << std::endl;std::string echo_string = "tcpserver echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size()); // 写}else if(n == 0){lg(Info, "%s:%d quit server close sockfd: %d", clientip.c_str(), clientport, sockfd);break;}else{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);break;}}}~TcpServer(){}
private:int listensock_; // 监听套接字uint16_t port_; // 端口std::string ip_; // IP地址
};

TcpClient.cc:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <unistd.h>#include <iostream>
#include <cstring>void Usage(const std::string& proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);// TCP客户端和UTP客户端是一样的,肯定是要bind的,只不过不需要显示的bind,由操作系统来进行绑定。int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));  // 客户端发起connect的时候,进行bind。if(n < 0){std::cerr << "connect error..." << std::endl;return 2;}std::string message;while(true){std::cout << "Please Enter# ";getline(std::cin, message);write(sockfd, message.c_str(), message.size());char inbuffer[4096];int n = read(sockfd, inbuffer, sizeof(inbuffer));if(n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}}close(sockfd);return 0;
}

Main.cc:

#include "TcpServer.hpp"#include <iostream>
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(UsageError);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(port));svr->InitServer();svr->Start();return 0;
}

Log.hpp:

#pragma once#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define SIZE 1024#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen; // 默认为屏幕打印path = "./log/";}void Enable(int method) // 更换日志的打印方式{printMethod = method;}std::string levelToString(int level) // 返回日志等级的字符串{switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt) // 日志打印{switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中{std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0){return;}write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件{std::string filename = LogFile;filename += '.';filename += levelToString(level);printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。time_t t = time(nullptr); // 返回值是时间戳struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。// 下面的\是续行符,加不加都行。snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE]; // 一条日志右边日志内容vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[SIZE * 2]; // 合成一条日志信息snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt);printLog(level, logtxt); // 打印}private:int printMethod; // 日志打印的方式std::string path; // 日志打印路径
};

2、会话

2.1、前台与后台进程

例如:对如下程序编译成可执行文件a.out。

#include <iostream>
using namespace std;#include <unistd.h>int main()
{while(true){cout << "hello world" << endl;sleep(1);}return 0;
}

我们可以使用如下方式启动一个后台进程,例如:

./a.out &

不过为了防止打印的干扰,我们可以使用重定向的方式将输出内容输出到文件中,例如:

./a.out >> log.txt &

启动后,命令行会显示一个如下内容:

其中1表示后台任务号,3990553表示进程的ID。

当再启动一个后台任务,显示如下内容:

任务号就变成了2,后面的数字依然是进程的ID。

我们可以使用jobs命令查看后台进程,例如:

使用fg命令+任务号的方式就可以把一个后台进程变成前台进程,例如:

我们可以使用ctrl+z的方式将前台进程暂停,并变成后台进程。例如:

使用jobs查看,如下图:

我们可以使用bg+任务号的方式,将上面的在后台暂停的进程重新启动执行,例如:

再用jobs查看,如下图:

2.2、进程组

例如:执行如下命令

./a.out >> log.txt &
sleep 1000 | sleep 2000 | sleep 3000 &

使用jobs命令查看,如下图所示:

使用如下命令:

ps ajx | head -1;ps ajx | grep -Ei 'a.out|sleep'

运行结果如下图所示:其中PPID和PID前面已经说过了;PGID是指进程组ID;SID就是会话ID也叫session ID;TTY表示当前对应的终端是谁。

从上面的图中可以看出,一个进程组中的组长就是多个进程中的第一个进程,比如三个sleep进程是一个进程组,其中第一个sleep进程是进程组的组长,进程组的ID就是进程组中组长进程的ID;a.out自成一个进程组,同时它也是进程组的组长。在同一个session内启动的进程组,session ID都是一样的,其中SID的256487就是bash,也就是说session ID和bash的进程ID是一样的,如下图所示

2.3、会话的概念

每个用户在Linux系统中进行登录的时候,Linux系统会形成一个会话,有几个登录就会形成几个会话,每个会话中都会默认有一个bash进程,这个bash是单进程的进程组(默认是前台进程组),用来执行输入的命令。因为系统中可能存在很多的登录,因此也就会有很多的会话,在操作系统中会话也是采用先描述再组织的方式进行管理的。如下图所示

注:一般而言一个会话中只能有一个前台进程组,可以有多个后台进程组。谁拥有键盘的输入谁就是前台进程组,其他的就是后台进程组,后台进程组不能从标准输入中获取数据。

当新执行一个前台进程组时,bash就变成了后台进程组,因此再输入命令时,就不会被执行;当新执行的前台进程组结束后,bash就又变成了前台进程组,也就可以继续执行命令。

注:命令行中任何时候都是要有一个前台进程组的。

2.4、会话关闭后的进程组

例如:对于上面运行的三个进程,退出登录,然后再重新登录,查看到如下内容

通过与之前的图片进行对比,我们可以发现后台的进程组并没有退出,PPID变成了1,TTY为?表示和终端没有关系了,会话仍被保留了(注意并不是所有的操作系统在用户退出后都会保留用户启动的后台进程组)。

我们可以看出,后台进程组是会受到用户的退出和登录的影响的,对于前台进程组来说更是如此。如不想要让进程组受到任何用户退出和登录影响的,就需要守护进程化

3、守护进程

3.1、守护进程化

让进程自成一个会话,就叫做守护进程。自成会话也就不受其他用户登录和退出的影响,因为会话之间是并列关系。

例如:创建一个会话,并将调用该函数的进程的ID设置为会话的ID。

#include <unistd.h>pid_t setsid(void);

该函数的作用就是让一个进程独立成一个会话。成功返回会话的ID,失败返回-1,错误码被设置。但是使用该函数有个要求就是,调用该函数的进程不能是进程组的组长

例如:我们可以实现一个函数让一个进程变成守护进程。

#pragma once
#include <iostream>
#include <cstdlib>
#include <csignal>
#include <string>#include <unistd.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Deamon(const std::string& cwd = "") // 守护进程化
{// 1、忽略一些异常信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGSTOP, SIG_IGN);// 2、将自己变成独立的会话if(fork() > 0) exit(0);setsid();// 3、更改当前进程的工作目录,默认进程的工作目录是进程启动时所处的目录if(!cwd.empty()){chdir(cwd.c_str());}// 4、将标准输入、标准输出、标准错误重定向至/dev/null中,这样再向标准输入、标准输出、标准错误写入的内容就会被丢弃int fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

注:守护进程的本质也是孤儿进程。对于守护进程,我们也是可以使用kill命令干掉的。

不过在系统中是有一个deamon函数的,该函数的作用就是将一个进程变成守护进程,例如:

#include <unistd.h>int daemon(int nochdir, int noclose);

其中,第一个参数为0表示工作在/目录下,其他数字表示工作在当前目录;第二个参数如果为0表示将标准输入、标准输出、标准错误重定向至/dev/null中,其他数字表示不会改变标准输入、标准输出、标准错误。

不过多数时候不会使用上面的接口,而是自己实现一个,因为自己实现的话可以灵活的根据具体情况进行一些调整。

3.2、应用

例如:对之前的TCP代码守护进程化,并使用线程池进行改造,做出一个可查询的词典出来。

Daemon.hpp:

#pragma once
#include <iostream>
#include <cstdlib>
#include <csignal>
#include <string>#include <unistd.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Deamon(const std::string& cwd = "") // 守护进程化
{// 1、忽略一些异常信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN); // 类似于管道,如果客户端直接关掉,再向文件描述符写入可能导致操作系统杀掉进程。signal(SIGSTOP, SIG_IGN);// 2、将自己变成独立的会话if(fork() > 0) exit(0);setsid();// 3、更改当前进程的工作目录,默认进程的工作目录是进程启动时所处的目录if(!cwd.empty()){chdir(cwd.c_str());}// 4、将标准输入、标准输出、标准错误重定向至/dev/null中,这样再向标准输入、标准输出、标准错误写入的内容就会被丢弃int fd = open(nullfile.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}
}

init.hpp:

#pragma once
#include <iostream>
#include <fstream>
#include <unordered_map>
#include <string>#include "Log.hpp"const std::string dictname = "./dict.txt";
const std::string sep = ":";static bool Split(std::string s, std::string* part1, std::string* part2) // 分割
{auto pos = s.find(sep);if(pos == std::string::npos) return false;*part1 = s.substr(0, pos);*part2 = s.substr(pos+1);return true;
}class Init
{
public:Init(){std::ifstream in(dictname); // 读文件if(!in.is_open()) // 判断是否打开文件{lg(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}std::string line;while(std::getline(in, line)){// 建立字典std::string part1, part2;Split(line, &part1, &part2);dict.insert({part1, part2});}in.close();}std::string translation(const std::string& key) // 翻译{auto iter = dict.find(key);if(iter == dict.end()){return "Unknow";}else{return iter->second;}}private:std::unordered_map<std::string, std::string> dict; // 字典
};

Log.hpp:

#pragma once#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define SIZE 1024#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen; // 默认为屏幕打印path = "./log/";}void Enable(int method) // 更换日志的打印方式{printMethod = method;}std::string levelToString(int level) // 返回日志等级的字符串{switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}void printLog(int level, const std::string &logtxt) // 日志打印{switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中{std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0){return;}write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件{std::string filename = LogFile;filename += '.';filename += levelToString(level);printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。time_t t = time(nullptr); // 返回值是时间戳struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。// 下面的\是续行符,加不加都行。snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE]; // 一条日志右边日志内容vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);char logtxt[SIZE * 2]; // 合成一条日志信息snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt);printLog(level, logtxt); // 打印}private:int printMethod; // 日志打印的方式std::string path; // 日志打印路径
};Log lg;

Main.cc:

#include "TcpServer.hpp"#include <iostream>
#include <memory>void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);exit(UsageError);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> svr(new TcpServer(port));svr->InitServer();svr->Start();return 0;
}

Task.hpp:

#pragma once
#include <iostream>
#include <string>
#include <cstdint>
#include <cstring>#include "Log.hpp"
#include "init.hpp"extern Log lg; // 日志
Init init; // 初始化字典class Task
{
public:Task(int sockfd, const std::string& clientip, const uint16_t& clientport):sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}void run(){char buffer[4096];ssize_t n = read(sockfd_, buffer, sizeof(buffer));if(n > 0) {buffer[n] = 0;std::cout << "client key# " << buffer << std::endl;std::string echo_string = init.translation(buffer);n = write(sockfd_, echo_string.c_str(), echo_string.size());if(n < 0){lg(Warning, "write error, errno: %d, errstring: %s", errno, strerror(errno));}}else if(n == 0){lg(Info, "%s:%d quit server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);}close(sockfd_);}void operator()(){run();}~Task(){}
private:int sockfd_;std::string clientip_;uint16_t clientport_;
};

TcpClient.cc:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <unistd.h>#include <iostream>
#include <cstring>void Usage(const std::string& proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);while(true){int cnt = 5;int isreconnect = false;int sockfd = 0;sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}do{// TCP客户端和UTP客户端是一样的,肯定是要bind的,只不过不需要显示的bind,由操作系统来进行绑定。// 客户端发起connect的时候,进行bind。int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));if(n < 0){isreconnect = true;cnt--;std::cerr << "connect error..., reconnect: " << cnt << std::endl;sleep(2); // 隔2秒重连一次}else{break;}}while(cnt && isreconnect);if(cnt == 0){std::cerr << "user offline..." << std::endl;break;}std::string message;std::cout << "Please Enter# ";getline(std::cin, message);int n = write(sockfd, message.c_str(), message.size());if(n < 0){std::cerr << "write error" << std::endl;continue;}char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if(n > 0){inbuffer[n] = 0;std::cout << inbuffer << std::endl;}close(sockfd);}return 0;
}

TcpServer.hpp:

#pragma once#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"#include <sys/socket.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>#include <csignal>
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <string>const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10;
extern Log lg;enum{ UsageError = 1, SocketError, BindError, ListenError };class TcpServer; // 声明class ThreadData // 线程数据
{
public:ThreadData(int fd, const std::string& ip, const uint16_t p, TcpServer* t):sockfd(fd), clientip(ip), clientport(p), tsvr(t){}public:int sockfd;std::string clientip;uint16_t clientport;TcpServer* tsvr;
};class TcpServer
{
public:TcpServer(const uint16_t& port, const std::string& ip = defaultip):listensock_(defaultfd), port_(port), ip_(ip){}void InitServer(){listensock_ = socket(AF_INET, SOCK_STREAM, 0);if(listensock_ < 0){lg(Fatal, "create socket errno: %d, errstring: %s", errno, strerror(errno));exit(SocketError);}lg(Info, "create socket sucess, listensock_: %d", listensock_);int opt = 1;setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 服务器无法立即重启的问题(后面讲TCP协议时再具体讲)struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port_);inet_aton(ip_.c_str(), &(local.sin_addr));//local.sin_addr.s_addr = INADDR_ANY;if(bind(listensock_, (const struct sockaddr*)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));exit(BindError);}lg(Info, "bind socket sucess, listensock_: %d", listensock_);// TCP是面向连接的,服务器要处于一种一直在等待连接的状态if(listen(listensock_, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "listen socket sucess, listensock_: %d", listensock_);}void Start(){Deamon("/");ThreadPool<Task>::GetInstance()->Start();lg(Info, "tcpServer running...");for(;;){// 1、获取新连接struct sockaddr_in client;socklen_t len = sizeof(client);int sockfd = accept(listensock_, (struct sockaddr*)&client, &len); if(sockfd < 0){lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno));continue;}uint16_t clientport = ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);// 2、根据新连接进行通信/** 线程池版本:使用线程池,采用短服务。*/Task t(sockfd, clientip, clientport);ThreadPool<Task>::GetInstance()->Push(t);}}~TcpServer(){}
private:int listensock_;uint16_t port_; // 端口号std::string ip_; // IP地址
};

ThreadPool.hpp:

#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>#include <pthread.h>
#include <unistd.h>struct ThreadInfo // 线程信息
{pthread_t tid;std::string name;
};static const int defaultnum = 6; // 默认线程个数template <class T>
class ThreadPool
{
public:void Lock() // 上锁{pthread_mutex_lock(&mutex_);}void Unlock() // 解锁{pthread_mutex_unlock(&mutex_);}void Wakeup() // 唤醒{pthread_cond_signal(&cond_);}void ThreadSleep() // 休眠{pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty() // 判断任务是否为空{return tasks_.empty();}std::string GetThreadName(pthread_t tid) // 获取线程名{for(const auto& ti : threads_){if(ti.tid == tid){return ti.name;}}return "None";}
public:// void* HandlerTask(void* args) // 这样写不行,因为该函数的第一个参数被ThreadPool* this占据了。// {                              //     while(true)//     {//         sleep(1);//         std::cout << "new thread wait task..." << std::endl;//     }// }static void* HandlerTask(void* args) // 我们也可以把这个函数放在类外{ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();while(tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}void Start(){int num = threads_.size();for(int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i+1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this); // 这里传this的原因是使用static修饰的}                                              //HandlerTask没法访问类内的成员,因此传个this指针就可以了}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T& t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T>* GetInstance() // 获取实例{if(nullptr == tp_) // 双重检查加锁{pthread_mutex_lock(&lock_);if(tp_ == nullptr){std::cout << "singleton ThreadPool first create!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defaultnum):threads_(num) // 初始化{pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}ThreadPool(const ThreadPool<T>&) = delete;ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;~ThreadPool() // 销毁{pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T>* tp_; // 改成懒汉模式。static pthread_mutex_t lock_; // 加锁,防止多线程同时创建多个实例。
};template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr; template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Makefile:

.PHONY:all
all: tcpserverd tcpclienttcpserverd:Main.ccg++ -o $@ $^ -std=c++11 -std=c++11 -gtcpclient:TcpClient.ccg++ -o $@ $^ -l pthread -g.PHONY: clean
clean:rm -f tcpserverd tcpclient

dict.txt:

benefit: 益处;受益
challenge: 挑战
disaster: 灾难
eager: 渴望的
fluent: 流利的
glance: 瞥一眼;扫视
hesitate: 犹豫
identity: 身份;同一性
journey: 旅行;旅程
mercy: 仁慈;宽恕
normal: 正常的;常态
occupy: 占据;占用
puzzle: 困惑;难题
react: 反应;回应
secure: 安全的;保护
tolerate: 容忍;忍受
unique: 独特的;唯一的
virtual: 虚拟的
wealthy: 富有的
zone: 区域;地带

注:一旦启动上面的服务,即使用户退出登录,服务依旧不会受到任何影响。另外常常给守护进程的名字末尾加一个d,比如端口号为22的服务叫ssh,对应的守护进程是sshd,这就是可以随时进行远程登录的原因。

4、TCP通信流程

下图是基于TCP协议的客户端和服务器程序进行通讯的一般流程,如图所示:

服务器初始化:调用socket,创建文件描述符;调用bind,将当前的文件描述符和ip和port绑定在一起;调用listen声明当前这个文件描述符,为后面的accept做好准备,调用accecpt并阻塞,等待客户端连接过来。

客户端与服务器建立连接的过程:调用socket,创建文件描述符;调用connect,向服务器发起连接请求,connect会发出SYN段并阻塞等待服务器应答(第一次握手); 服务器收到客户端的SYN,会应答一个SYN-ACK段表示同意建立连接(第二次握手); 客户端收到SYN-ACK后会从connect函数返回,同时应答一个ACK(第三次握手),随后accept()返回。这个建立连接的过程,通常称为三次握手

数据传输的过程:服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待;这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

注:TCP协议提供全双工的通信服务,所谓全双工的意思是,在同一条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据。原因是TCP是分别有发送缓冲区和接收缓冲区的,如下图所示:

注:read和write函数本质上是一种拷贝函数,也就是把用户数据拷贝到发送缓冲区或者把接收缓冲区的数据读出来。

客户端与服务器断开连接的过程:如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次挥手);此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次挥手);read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送 一个FIN(第三次挥手);客户端收到FIN,再返回一个ACK给服务器(第四次挥手)。这个断开连接的过程,通常称为四次挥手

注:服务器和客户端是可能存在大量的链接的,操作系统也要对这些链接进行管理,也是通过先描述再组织的方式进行管理的。

http://www.dtcms.com/a/611507.html

相关文章:

  • seo网站优化培训多少价格中国建设银行app官方下载
  • 临沂网站建设那家好建设网站需要专业
  • 软考~系统规划与管理师考试——真题篇——章节——第18章 智慧城市发展规划——纯享题目版
  • 站长工具seo综合查询怎么使用的精品网站建设费用 找磐石网络一流
  • 做宣传图片的网站微信小程序开发视频完整教程
  • Linux环境变量持久化完全指南
  • 电商网站前端制作分工西宁做网站需要多少钱
  • dede鲜花网站模板下载石家庄企业网站制作哪家好
  • 织梦网站搬家教程怎么百度推广
  • Linux网络数据链路层
  • 苹果iOS测试版描述文件详细安装步骤指南
  • 百度收录好的免费网站保险查询平台
  • 莱州网站定制wordpress 粘贴表格
  • 织梦做的网站网速打开慢是怎么回事网站模板和定制的区别
  • 织梦网址导航网站模板wordpress电商
  • jQuery Accordion:高效且实用的网页交互组件
  • 找别人做网站注意什么做免费的视频网站可以赚钱吗
  • 做市场调查分析的网站网站域名怎么看
  • 一键部署MySQL全攻略
  • 搭建局域网MQTT通信
  • C++进阶 -- set、map、multiset、multimap的介绍及使用
  • 辽宁省朝阳市做网站首饰行业网站建设策划
  • 杭州网站开发工资企业网站seo营销
  • 特色的南昌网站制作做网站主题
  • 哈尔滨企业建站系统移动服务器建设的电影网站
  • 开发手机网站多少钱保定市网站建设
  • 设计配色的网站免费高清素材网站
  • 秦皇岛做网站公司有哪些网站黑链怎么做的
  • 网站问题seo解决方案网站如何做tag
  • 班级网站建设步骤平面设计专业的大专院校