> 文章列表 > 【Linux】网络编程之套接字 --TCP

【Linux】网络编程之套接字 --TCP

【Linux】网络编程之套接字 --TCP

目录

  • 🌈前言
  • 🌸1、TCP相关API
    • 🍡1.1、socket函数
    • 🍢1.2、bind函数
    • 🍧1.3、listen函数
    • 🍨1.4、accept函数
    • 🍰1.5、connect函数
  • 🌺2、TCP网络编程
    • 🍡2.1、简单TCP通信程序 -- 多进程版本
    • 🍢2.2、简单TCP通信程序 -- 多线程版本
    • 🍧2.3、简单TCP通信程序 -- 线程池版本
  • 🍀3、部署服务器
    • 🍡3.1、会话和进程组
    • 🍢3.2、守护/精灵进程
    • 🍧3.3、实现守护进程
    • 🍨3.4、部署服务器

🌈前言

这篇文章给大家带来套接字的学习!!!


🌸1、TCP相关API

      • 【UDP中已经详细介绍了背景知识和struct sockaddr结构:跳转】

使用TCP网络通信所用到的接口比UDP多了几个,因为TCP是需要进行连接的(面向连接)

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);// 将套接字设置为监听用户到来的连接请求状态,监听是否有客户到来
int listen(int sockfd, int backlog);// 服务器启动后,获取新的客户端的连接,该函数返回新的服务套接字,用于服务客户的请求(流式IO服务)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 客户端向指定服务器发送连接请求
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);// 指定填充确定IP地址, 转化字符串风格IP("xx.zz.yy.sss"),并且自动进行主机字节序转换网络字节序
in_addr_t inet_addr(const char *cp));

🍡1.1、socket函数

【Linux】网络编程之套接字 --TCP

函数解析

  • 作用:它是用来创建套接字文件的,创建成功返回一个文件描述符失败返回-1,并且设置errno

  • domain:它需要我们填充协议家族(地址类型)来指定网络协议,IPv4指定为:AF_INET

  • type:它需要我们填充通信类型,UDP协议是面向数据报的,我们填充SOCK_DGRAM即可,如果是TCP协议,那么填充SOCK_STREAM(其他详细查看man手册)

  • protocol:套接口所用的协议,一般为0,不指定

void Test()
{// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0){std::cout << "socket error: " << strerror(errno) << std::endl;exit(1);}
}

🍢1.2、bind函数

【Linux】网络编程之套接字 --TCP

函数解析

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,服务器需要调用bind绑定一个固定的网络地址和端口号

  • 作用:绑定网络信息(地址类型、端口号和IP地址)到内核中,成功返回0,错误返回-1,并且设置errno

  • addr:传sockaddr_in/sockaddr_un的地址,并且需要强转为通用类型sockaddr*

  • addrlen:sockaddr_in/sockaddr_un结构体所占内存空间的大小

void Test(uint16_t port, const string &ip)
{int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0){cout << "socket error: " << strerror(errno) << endl;exit(1);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                     // "地址类型/协议家族"Server.sin_port = htons(port);                                                   // 端口号Server.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str()); // IP地址if (bind(fd, (const struct sockaddr *)&Server, len) < 0){std::cout << "bind error: " << strerror(errno) << std::endl;exit(1);}
}

🍧1.3、listen函数

【Linux】网络编程之套接字 --TCP

函数解析

  • 作用:将套接字fd设置为监听到来的“连接请求状态”监听是“否有”客户到来(TCP -> 面向连接)

  • sockfd:套接字文件描述符,socket函数的返回值

  • backlog:套接字的挂起"连接队列"的最大长度(一般由2到4),用SOMAXCONN则为系统给出的最大值(最多允许有backlog个客户端处于连接等待状态)

  • 返回值:成功返回0,失败返回-1,并且设置全局errno错误码

void Test(uint16_t port, const string &ip)
{int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0){cout << "socket error: " << strerror(errno) << endl;exit(1);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                     // "地址类型/协议家族"Server.sin_port = htons(port);                                                   // 端口号Server.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str()); // IP地址if (bind(fd, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)// 第二个参数:套接字的挂起"连接队列"的最大长度(一般由2到4), 用SOMAXCONN则为系统给出的最大值if (listen(fd, SOMAXCONN) < 0){std::cout << "listen error: " << strerror(errno) << std::endl;exit(3);}
}

🍨1.4、accept函数

【Linux】网络编程之套接字 --TCP

函数解析

  • 作用:三次握手完成后(后面网络基础再解析), 服务器调用accept()接受连接

  • 如果服务器调用accept()时还没有客户端的连接请求,就一直阻塞等待直到有客户端连接到来

  • sockfd:套接字文件描述符,socket函数的返回值

  • addr是一个输出型参数,它由accept函数内部自动填充,它用于获取客户端的IP地址和端口号

  • addrlen参数是一个输入输出参数,输入需要调用者提供缓冲区addr的长度,以避免缓冲区溢出问题,输出的是客户端地址结构体的实际长度

  • 该函数返回新的“服务”套接字,用于服务客户端的请求和回应(流式IO服务)

void Test(uint16_t port, const string &ip)
{int fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0){cout << "socket error: " << strerror(errno) << endl;exit(1);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                     // "地址类型/协议家族"Server.sin_port = htons(port);                                                   // 端口号Server.sin_addr.s_addr = ip.empty() ? htonl(INADDR_ANY) : inet_addr(ip.c_str()); // IP地址if (bind(fd, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(2);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)if (listen(fd, SOMAXCONN) < 0){std::cout << "Server listen error: " << strerror(errno) << std::endl;exit(3);}// 4. 获取客户端的连接请求 -- 这里没有写循环struct sockaddr_in Client;socklen_t len = sizeof(Client);memset(&Client, 0, len);int Serverfd = accept(fd, (sockaddr*)&Client, &len);if (Serverfd < 0){cout << "accept error: " << strerror(errno) << endl;exit(4);}close(Serverfd );
}

🍰1.5、connect函数

【Linux】网络编程之套接字 --TCP

函数解析

  • 作用:客户端向指定服务器发送连接请求,默认自动绑定本主机的sockaddr_in信息

  • 最好不要自己bind,因为我们绑定的固定端口号可能被其他客户端给占用了,由OS自动分配即可

  • connect和bind的参数一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址

  • 返回值:connect()成功返回0,出错返回-1,并且设置全局errno

void Init(uint16_t port_, const string ip_)
{// 1. 获取sockfdint sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 填充指定Server的sockaddr_in信息struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;Server.sin_port = htons(port_);Server.sin_addr.s_addr = inet_addr(ip_.c_str());// 2. 向指定服务器发送连接请求 -- connet默认会自动绑定本主机的sockaddr_in信息// 最好不要自己bind,因为我们绑定的固定端口号可能被其他客户端给占用了,由OS自动分配即可if (connect(sockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Client connet Server error: " << strerror(errno) << std::endl;exit(1);}
}

🌺2、TCP网络编程

🍡2.1、简单TCP通信程序 – 多进程版本

实现大小写转换服务器 – 使用TCP协议

  • 注意:TCP于UDP不同,UDP是不需要连接的,服务器请求到来时会立刻处理(recvfrom和sendto),多用户发送请求时,是不会阻塞住的!!!

  • TCP是需要连接的,设置监听套接字后,在等待获取用户连接时是会阻塞的,如果只用一个死循环进行获取请求和服务,那么将会导致其他用户连接不了,因为要等第一个用户的请求工作完了才行

  • 这里我们设置一个多进程,父进程进行获取请求,子进程用来进行服务

下面服务器提供服务的API使用多进程来实现的

util.h – 用于保存相同的头文件

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>#define BUFFER 1024
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define FORM_ERR 4
#define CONNET_ERR 5

TcpServer.cc – 服务器源代码

#include "until.h"class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): ListenSockfd_(-1), port_(port), ip_(ip){}~TcpServer(){if (ListenSockfd_ > 2){close(ListenSockfd_);}}public:void Init(){// 1. 获取sockfd// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议ListenSockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (ListenSockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                       // "地址类型/协议家族"Server.sin_port = htons(port_);                                                    // 端口号Server.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); // IP地址if (bind(ListenSockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)// 第二个参数:套接字的挂起"连接队列"的最大长度(一般由2到4), 用SOMAXCONN则为系统给出的最大值if (listen(ListenSockfd_, SOMAXCONN) < 0){std::cout << "Server listen error: " << strerror(errno) << std::endl;exit(LISTEN_ERR);}}void Start(){int ServerSockfd;while (true){//-------------------------------------------------------------------------------------// 4. 服务器启动后,获取新的客户端的连接// 该函数返回新的服务套接字,用于服务客户的请求(流式IO服务)struct sockaddr_in Client;socklen_t len = sizeof(Client);ServerSockfd = accept(ListenSockfd_, (struct sockaddr *)&Client, &len);if (ServerSockfd < 0){std::cout << "Server get connection for error" << strerror(errno) << std::endl;continue;}// 获取Client网络信息 -- port、ip address -- 网络序列 <-> 主机序列std::string ClientPort = std::to_string(ntohs(Client.sin_port));std::string ClientIp = inet_ntoa(Client.sin_addr);//-------------------------------------------------------------------------------------//-------------------------------------------------------------------------------------// 实现多客户端连接 -- 如果不使用多进程,只能对单个进程提供服务int id = fork();signal(SIGCHLD, SIG_IGN); // 忽略子进程的退出状态, 子进程退出直接释放if (id == 0){// 给客户端提供服务 -- 子进程做Proservice(ClientPort, ClientIp, ServerSockfd);exit(0);}// 父进程不能使用waitpid阻塞等待,上面已经设置捕捉SIGCHLD信号且忽略它了!//-------------------------------------------------------------------------------------}}public:void Proservice(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd){char inbuffer[BUFFER];std::string outbuffer;while (true){// 5. 读取Client发送的数据 -- TCP->流式socket->文件流IOmemset(inbuffer, 0, BUFFER);ssize_t Read = read(ServerSockfd, inbuffer, BUFFER - 1);if (Read > 0){inbuffer[Read] = 0;// 客户端输入quit时退出本次服务 -- 不区分大小写if (strcasecmp(inbuffer, "quit") == 0){std::cout << "Client[" << ClientPort << ":" << ClientIp<< "]# exit server!!!" << std::endl;break;}std::cout << "[" << ClientPort << ":" << ClientIp << "]echo# "<< inbuffer << std::endl;}else if (Read == 0) // 读端一直读,写端关闭,读端read返回0 -- Read == 0 -> 对方退出{std::cout << "[" << ClientPort << ":" << ClientIp << "]exit! ! !" << std::endl;break;}else{std::cout << "Server read data error" << strerror(errno) << std::endl;break;}// 提供服务 -- 小写英文 -> 大写英文 -- 大小写转换LowToUpp(inbuffer, outbuffer);// 6. 发送数据到Client -- TCP->流式socket->文件流IOssize_t Write = write(ServerSockfd, (void *)outbuffer.c_str(), outbuffer.size());if (Write < 0){std::cout << "Server write data error" << strerror(errno) << std::endl;continue;}outbuffer.clear(); // 清空数据}close(ServerSockfd); // 服务完成后,关闭"服务socket fd"}// 大小写转换服务void LowToUpp(const std::string &src, std::string &fast){for (auto ch : src){if (ch >= 'a' && ch <= 'z'){fast += ch - 32;}else{fast += ch;}}}private:int ListenSockfd_; // 监听socket fduint16_t port_;std::string ip_;
};// (./tcpserver port [ip])
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){std::cout << "Command Line Format: ./tcpserver port [ip]" << std::endl;exit(FORM_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}TcpServer Server(port, ip);Server.Init();Server.Start();return 0;
}

TcpClient.cc – 客户端源代码

  • 这里还是要说一下:客户端最好不要自己bind网络信息,由OS自动分配即可,因为本机的其他客户端可能占用了你需要绑定的固定端口号

  • 然而,这只是一个建议,如果你要自己bind,那你可以试一下

#include "until.h"class TcpClient
{
public:TcpClient(uint16_t port, std::string ip = ""): sockfd_(-1), port_(port), ip_(ip){}~TcpClient(){if (sockfd_ > 2){close(sockfd_);}}public:void Init(){// 1. 获取sockfd// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议sockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (sockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 填充指定Server的sockaddr_in信息struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;Server.sin_port = htons(port_);Server.sin_addr.s_addr = inet_addr(ip_.c_str());// 2. 向指定服务器发送连接请求 -- connet默认会自动绑定本主机的sockaddr_in信息// 最好不要自己bind,因为我们绑定的固定端口号可能被其他客户端给占用了,由OS自动分配即可if (connect(sockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Client connet Server error: " << strerror(errno) << std::endl;exit(CONNET_ERR);}}void Start(){std::string outbuffer;char inbuffer[BUFFER];while (true){// 3. 发送数据到Server -- TCP->流式socket->文件流IOstd::cout << "echo# ";fflush(stdout);std::getline(std::cin, outbuffer);ssize_t Write = write(sockfd_, (void *)outbuffer.c_str(), outbuffer.size());if (Write < 0){std::cout << "Client write data error" << strerror(errno) << std::endl;break;}// 输入quit时,退出客户端 -- 不区分大小写if (strcasecmp(inbuffer, "quit") == 0){std::cout << "Client exit!!!" << std::endl;break;}// 4. 读取Server发送的数据 -- TCP->流式socket->文件流IOmemset(inbuffer, 0, BUFFER);ssize_t Read = read(sockfd_, inbuffer, BUFFER - 1);if (Read > 0){inbuffer[Read] = 0;std::cout << "Server data echo# " << inbuffer << std::endl;}else if (Read == 0) // 读端一直读,写端关闭,读端read返回0 -- Read == 0 -> 对方退出{break;}else{std::cout << "Client read data error" << strerror(errno) << std::endl;break;}}}private:int sockfd_;uint16_t port_;std::string ip_;
};// (./tcpclient port ip)
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Command Line Format: ./tcpserver port ip" << std::endl;exit(FORM_ERR);}uint16_t port = atoi(argv[1]);std::string ip;ip = argv[2];TcpClient Client(port, ip);Client.Init();Client.Start();return 0;
}

🍢2.2、简单TCP通信程序 – 多线程版本

实现大小写转换服务器 – 使用TCP协议

  • 多线程版本:引入多线程,解决服务器提供服务时只能串行化的去执行代码

  • 使用多线程并发的去执行服务,不需要关心线程安全问题,线程执行函数(服务)是不需要访问临界资源的,它是一个可重入的函数!!!

  • 需要注意的是:在类中定义线程执行函数,函数内部会有隐藏this指针,需要使用static修饰,也可以在类外部定义线程执行函数

  • 思路:创建一个结构体保存客户端的网络信息和客户端类的this指针,Server类定义这个结构体保存Client数据和自己的this,然后将这个结构体地址传给线程,后面的代码详细查看


TcpServer.cc – 服务器源代码 – 其他不变

#include "until.h"class TcpServer; // 前置声明,不开辟空间class ClientData
{
public:ClientData(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd, TcpServer *This): ClientPort_(ClientPort),ClientIp_(ClientIp),ServerSockfd_(ServerSockfd),This_(This){}public:std::string ClientPort_;std::string ClientIp_;int ServerSockfd_;TcpServer *This_;
};class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): ListenSockfd_(-1), port_(port), ip_(ip){}~TcpServer(){if (ListenSockfd_ > 2){close(ListenSockfd_);}}public:void Init(){// 1. 获取sockfd// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议ListenSockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (ListenSockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                       // "地址类型/协议家族"Server.sin_port = htons(port_);                                                    // 端口号Server.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); // IP地址if (bind(ListenSockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)// 第二个参数:套接字的挂起"连接队列"的最大长度(一般由2到4), 用SOMAXCONN则为系统给出的最大值if (listen(ListenSockfd_, SOMAXCONN) < 0){std::cout << "Server listen error: " << strerror(errno) << std::endl;exit(LISTEN_ERR);}}void Start(){int ServerSockfd;while (true){//-------------------------------------------------------------------------------------// 4. 服务器启动后,获取新的客户端的连接// 该函数返回新的服务套接字,用于服务客户的请求(流式IO服务)struct sockaddr_in Client;socklen_t len = sizeof(Client);ServerSockfd = accept(ListenSockfd_, (struct sockaddr *)&Client, &len);if (ServerSockfd < 0){std::cout << "Server get connection for error" << strerror(errno) << std::endl;continue;}// 获取Client网络信息 -- port、ip address -- 网络序列 <-> 主机序列std::string ClientPort = std::to_string(ntohs(Client.sin_port));std::string ClientIp = inet_ntoa(Client.sin_addr);//-------------------------------------------------------------------------------------//-------------------------------------------------------------------------------------// 实现多客户端连接 -- 多线程版本 -- 提供服务需要Client的port、ip、sock fdClientData* pcd = new ClientData(ClientPort, ClientIp, ServerSockfd, this);pthread_t tid;// 因为在类中定义线程执行函数,需要加static,不能访问服务函数,只能在ClientData添加本类的this指针pthread_create(&tid, nullptr, ThreadHandler, pcd);//-------------------------------------------------------------------------------------}}public:static void *ThreadHandler(void *args){// 分离线程 -- 如果主线程进行等待会阻塞住pthread_detach(pthread_self());ClientData *pcd = static_cast<ClientData*>(args);// 提供客户服务pcd->This_->Proservice(pcd->ClientPort_, pcd->ClientPort_, pcd->ServerSockfd_);delete pcd;return nullptr;}void Proservice(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd){char inbuffer[BUFFER];std::string outbuffer;while (true){// 5. 读取Client发送的数据 -- TCP->流式socket->文件流IOmemset(inbuffer, 0, BUFFER);ssize_t Read = read(ServerSockfd, inbuffer, BUFFER - 1);if (Read > 0){inbuffer[Read] = 0;// 客户端输入quit时退出本次服务 -- 不区分大小写if (strcasecmp(inbuffer, "quit") == 0){std::cout << "Client[" << ClientPort << ":" << ClientIp<< "]# exit server!!!" << std::endl;break;}std::cout << "[" << ClientPort << ":" << ClientIp << "]echo# "<< inbuffer << std::endl;}else if (Read == 0) // 读端一直读,写端关闭,读端read返回0 -- Read == 0 -> 对方退出{std::cout << "[" << ClientPort << ":" << ClientIp << "]exit! ! !" << std::endl;break;}else{std::cout << "Server read data error" << strerror(errno) << std::endl;break;}// 提供服务 -- 小写英文 -> 大写英文 -- 大小写转换LowToUpp(inbuffer, outbuffer);// 6. 发送数据到Client -- TCP->流式socket->文件流IOssize_t Write = write(ServerSockfd, (void *)outbuffer.c_str(), outbuffer.size());if (Write < 0){std::cout << "Server write data error" << strerror(errno) << std::endl;continue;}outbuffer.clear(); // 清空数据}close(ServerSockfd); // 服务完成后,关闭"服务socket fd"}void LowToUpp(const std::string &src, std::string &fast){for (auto ch : src){if (ch >= 'a' && ch <= 'z'){fast += ch - 32;}else{fast += ch;}}}private:int ListenSockfd_; // 监听socket fduint16_t port_;std::string ip_;
};// (./tcpserver port [ip])
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){std::cout << "Command Line Format: ./tcpserver port [ip]" << std::endl;exit(FORM_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}TcpServer Server(port, ip);Server.Init();Server.Start();return 0;
}

🍧2.3、简单TCP通信程序 – 线程池版本

实现大小写转换服务器 – 使用TCP协议

优化

  • 上面2.2的多线程版本,每次客户的请求到来都要去申请线程,这样太慢了(效率低)

  • 我们可以设计一个线程池,好处:可以提前申请好多个线程,客户请求到来时可以立刻去执行

线程池

  • 线程池:启动一个或一个以上的线程,等待任务到来,来任务就派发线程去执行任务

  • 优点:提高效率,提前启动线程

Threadpool.hpp – 线程池源代码 – 非模板版本

#include "Task.hpp"
#include <iostream>
#include <queue>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>// 线程池 -- 启动一堆线程,等待任务到来,来任务就派发线程去执行任务
// 优先:提高效率,提前启动线程// 设置方案:启动N个线程 -- 执行线程处理函数 -- 函数里面加锁判断队列是否有任务(条件)
//                     -- 没有任务则在条件变量下阻塞等待,有任务则到pop函数拿任务
//                     -- push函数用于加载任务,并且让条件变量就绪,唤醒线程,处理加载的任务const int TNum = 5; // 默认设置线程数据为5个class ThreadPool
{
private:// 构造函数与析构函数ThreadPool(int ThreadNum = TNum) : IsStart(false), ThreadNum_(ThreadNum){// 线程数量不能小于或等于0if (ThreadNum_ < 0){std::cout << "The number of threads cannot be less than 1 " << std::endl;exit(1);}pthread_mutex_init(&Pmutex_, nullptr);pthread_cond_init(&Pcond_, nullptr);}ThreadPool(const ThreadPool &) = delete;ThreadPool &operator=(const ThreadPool &) = delete;public:~ThreadPool(){pthread_mutex_destroy(&Pmutex_);pthread_cond_destroy(&Pcond_);}public:// 单例模式static ThreadPool *GetThreadPool(){if (ptp == nullptr){pthread_mutex_t Singleton_ = PTHREAD_MUTEX_INITIALIZER; // 单例锁 -- 局部初始化pthread_mutex_lock(&Singleton_);if (ptp == nullptr){ptp = new ThreadPool();ptp->StartUpThread(); // 启动线程池}pthread_mutex_unlock(&Singleton_);}return ptp;}public:// 线程处理任务函数 -- 设置全局处理函数 -- 类成员函数包含隐藏this指针static void *TaskProcess(void *args){pthread_detach(pthread_self());ThreadPool *tpp = static_cast<ThreadPool *>(args);while (true){tpp->lockQueue();            // 加锁while (!(tpp->IshaveTask())) // 判断是否有任务,有返回1,没有返回0{// 没有任务就在条件变量下阻塞等待,等待push加载任务后唤醒// 阻塞时,会释放互斥锁,条件变量就绪被唤醒后,会重新获取锁资源tpp->waitForTask();}// 这个任务就被拿到了线程的上下文中Task t(tpp->pop());tpp->unlockQueue(); // 解锁// ....下面是并发的处理任务 -- 约定好任务类必须有"函数对象(operator()())"t();}return nullptr;}// 启动多线程void StartUpThread(){if (IsStart != false){std::cout << "Error: Start thread repeatedly! ! !" << std::endl;exit(2);}for (int i = 0; i < ThreadNum_; ++i){pthread_t tid;// 这里传this指针,是因为处理函数是在类里面(类成员函数包含隐藏this指针)-- error: 类型不符合pthread_create(&tid, nullptr, TaskProcess, this);}// 启动完成,更新IsStart为trueIsStart = true;}// 加载任务void push(Task &val){// 加锁 -- 加载任务 -- 派发任务给线程 --解锁lockQueue();threadpool_.push(val);WakeUp();unlockQueue();}private:// 封装部分mutex、cond的接口void lockQueue() { pthread_mutex_lock(&Pmutex_); }void unlockQueue() { pthread_mutex_unlock(&Pmutex_); }void WakeUp() { pthread_cond_signal(&Pcond_); }void waitForTask() { pthread_cond_wait(&Pcond_, &Pmutex_); }bool IshaveTask() { return !(threadpool_.empty()); }// 线程处理任务后,需要在队列中删除任务 -- 并且返回这个任务(处理函数要对任务进行处理)Task pop(){Task val(threadpool_.front());threadpool_.pop();return val;}private:bool IsStart;                 // 防止重复启动线程int ThreadNum_;               // 线程数量std::queue<Task> threadpool_; // 线程池pthread_mutex_t Pmutex_;      // 互斥锁pthread_cond_t Pcond_;        // 条件变量
public:static ThreadPool *ptp;
};ThreadPool *ThreadPool::ptp = nullptr;

Task.h – 任务类

为什么要使用function函数封装器呢>?

  • 因为如果服务器的服务API在本类中定义的话,传参给Task类时,服务API有隐藏this指针,我们只能通过bind来调整参数和function来接收

  • function是一个通用的函数封装器,可以封装任何可调用的API(普通函数、成员函数、函数对象等),bind可以调整可调用函数的参数,调整完后生成新的可调用函数,二个是配套使用的

  • 下面的例子的服务API是定义在类外部的,不使用function也行,因为是一个普通函数,没有隐藏的指针,只需要将函数名传过去即可,但我还是用了function接收参数

#include "until.h"
#include <functional>class Task
{
private:// 类型重命名using ServerFunc = std::function<void(const std::string, const std::string, int)>;public:Task(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd, ServerFunc svf): ClientPort_(ClientPort),ClientIp_(ClientIp),ServerSockfd_(ServerSockfd),svf_(svf){}void operator()() // 函数对象{// 调用函数对象svf_(ClientPort_, ClientIp_, ServerSockfd_);}private:std::string ClientPort_;std::string ClientIp_;int ServerSockfd_;ServerFunc svf_; // 函数封装器
};

TcpServer.cc – 服务器源代码 – 其他不变

#include "until.h"
#include "Threadpool.hpp"// -------------------------------- 服务 --------------------------------------------------------------
void LowToUpp(const std::string &src, std::string &fast) // 英文大小写转换服务
{for (auto ch : src){if (ch >= 'a' && ch <= 'z'){fast += ch - 32;}else{fast += ch;}}
}void Proservice(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd)
{char inbuffer[BUFFER];std::string outbuffer;while (true){// 前四步在TcpServer.cc的Start函数中// 5. 读取Client发送的数据 -- TCP->流式socket->文件流IOmemset(inbuffer, 0, BUFFER);ssize_t Read = read(ServerSockfd, inbuffer, BUFFER - 1);if (Read > 0){inbuffer[Read] = 0;// 客户端输入quit时退出本次服务 -- 不区分大小写if (strcasecmp(inbuffer, "quit") == 0){std::cout << "Client[" << ClientPort << ":" << ClientIp<< "]# exit server!!!" << std::endl;break;}std::cout << "[" << ClientPort << ":" << ClientIp << "]echo# "<< inbuffer << std::endl;}else if (Read == 0) // 读端一直读,写端关闭,读端read返回0 -- Read == 0 -> 对方退出{std::cout << "[" << ClientPort << ":" << ClientIp << "]exit! ! !" << std::endl;break;}else{std::cout << "Server read data error" << strerror(errno) << std::endl;break;}// 提供服务 -- 小写英文 -> 大写英文 -- 大小写转换LowToUpp(inbuffer, outbuffer);// 6. 发送数据到Client -- TCP->流式socket->文件流IOssize_t Write = write(ServerSockfd, (void *)outbuffer.c_str(), outbuffer.size());if (Write < 0){std::cout << "Server write data error" << strerror(errno) << std::endl;continue;}outbuffer.clear(); // 清空数据}close(ServerSockfd); // 服务完成后,关闭"服务socket fd"
}
// ---------------------------------------------------------------------------------------------------// ------------------------------------- Client Data -------------------------------------------------
class TcpServer; // 前置声明,不开辟空间class ClientData // 客户端网络数据
{
public:ClientData(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd, TcpServer *This): ClientPort_(ClientPort),ClientIp_(ClientIp),ServerSockfd_(ServerSockfd),This_(This){}public:std::string ClientPort_;std::string ClientIp_;int ServerSockfd_;TcpServer *This_;
};
// ---------------------------------------------------------------------------------------------------class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): ListenSockfd_(-1), port_(port), ip_(ip){}~TcpServer(){if (ListenSockfd_ > 2){close(ListenSockfd_);}delete ptp;}public:void Init(){// 1. 获取sockfd// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议ListenSockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (ListenSockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                       // "地址类型/协议家族"Server.sin_port = htons(port_);                                                    // 端口号Server.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); // IP地址if (bind(ListenSockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)// 第二个参数:套接字的挂起"连接队列"的最大长度(一般由2到4), 用SOMAXCONN则为系统给出的最大值if (listen(ListenSockfd_, SOMAXCONN) < 0){std::cout << "Server listen error: " << strerror(errno) << std::endl;exit(LISTEN_ERR);}}void Start(){int ServerSockfd;while (true){//-------------------------------------------------------------------------------------// 4. 服务器启动后,获取新的客户端的连接// 该函数返回新的服务套接字,用于服务客户的请求(流式IO服务)struct sockaddr_in Client;socklen_t len = sizeof(Client);ServerSockfd = accept(ListenSockfd_, (struct sockaddr *)&Client, &len);if (ServerSockfd < 0){std::cout << "Server get connection for error" << strerror(errno) << std::endl;continue;}// 获取Client网络信息 -- port、ip address -- 网络序列 <-> 主机序列std::string ClientPort = std::to_string(ntohs(Client.sin_port));std::string ClientIp = inet_ntoa(Client.sin_addr);//-------------------------------------------------------------------------------------//-------------------------------------------------------------------------------------// 线程池版本Task t(ClientPort, ClientIp, ServerSockfd, Proservice); // Proservice是普通函数,不需要bind调整ptp = ThreadPool::GetThreadPool();ptp->push(t);//-------------------------------------------------------------------------------------}}private:int ListenSockfd_; // 监听socket fduint16_t port_;std::string ip_;ThreadPool *ptp;
};// (./tcpserver port [ip])
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){std::cout << "Command Line Format: ./tcpserver port [ip]" << std::endl;exit(FORM_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}TcpServer Server(port, ip);Server.Init();Server.Start();return 0;
}

makefile

all:tcpserver tcpclienttcpclient:TcpClient.ccg++ -o $@ $^ -std=c++11 -lpthreadtcpserver:TcpServer.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf tcpserver tcpclient

🍀3、部署服务器

🍡3.1、会话和进程组

进程组:

  • 进程组:顾名思义,从”组“中可以看出是一个进程的组合,进程组包含一个或多个进程

  • 进程组组长:每个进程组都有一个“组长”,这个组长是进程组中第一个启动的进程,其他进程都是进程组的组员

  • 进程组的生命周期与组长无关,只有当组长和组员全部退出后,这个进程组才会被释放

  • 进程组组长的PGID = 本身的PID,组员PGID = 第一个启动的进程的PID

【Linux】网络编程之套接字 --TCP

会话:

  • 会话:会话是由一个前台进程组(必须)和多个后台进程组组成的

  • 我们登录shell时,OS会我们构建一个会话,会话需要加载bash解析器,bash就是会话话首的进程组,后续我们新启进程时,就是在bash上启动的子进程,bash的PID就是会话的ID(SID)

  • 会话的生命周期是随用户的,用户退出它就退出

  • 在bash中只能启动一个前台进程,但是可以启动多个后台进程,因为前台进程会占用终端,它会将我们阻塞住,直到完成指令的运行

【Linux】网络编程之套接字 --TCP


🍢3.2、守护/精灵进程

【跳转 – 守护/精灵进程 – 百度百科】

  • 守护进程:它是一个后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束

  • 原理:守护进程将自己所在的会话中剥离出来,生成一个新的会话,自己就是会话的话首。它是一个在后台运行并且不受任何终端控制的进程

创建步骤

  • 忽略信号:根据特定的业务和代码逻辑,忽略特定的信号,防止程序异常崩溃

  • 设置新的进程当前工作路径(CWD):使用chdir函数改变进程工作路径

  • 创建会话,会话话首是当前的进程,我们使用setsid函数即可

  • 关闭输入、输出、错误流文件描述符

  • 打开/dev/null(垃圾桶)文件,将输入、输出、错误流文件描述符重定向到/dev/null

注意:setsid不能是使用进程组组长调用,否则会出错!!!


🍧3.3、实现守护进程

Daemon.hpp

#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>// 参数:进程当前工作路径(cwd),默认是原来的路径
void Daemon(const std::string& newCwd = "")
{// 1. 忽略SICPIPE信号 -- Server写 -> Client读端关闭 -> Server的write会受到SIGPIPE信号导致终止signal(SIGPIPE, SIG_IGN);// 2. 设置新的process cwdif (newCwd != ""){chdir(newCwd.c_str());}// 3. 创建新的会话,话首是服务器进程 -- setsid:设置会话的进程不能是进程组组长if (fork() > 0) exit(0);setsid();// 4. 将IO和错误流重定向至/dev/null文件中(数据黑洞)int fd  =open("/dev/null", O_RDWR);if (fd > 0){dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 重定向成功后,关闭fdif (fd > STDERR_FILENO){close(fd);}}
}

🍨3.4、部署服务器

部署服务器只要将守护进程代码在主函数中执行,就完成部署了!!!

TcpServer – 其他的不变 – 线程池版本的通信程序

#include "until.h"
#include "Threadpool.hpp"
#include "Daemon.hpp"// -------------------------------- 服务 --------------------------------------------------------------
void LowToUpp(const std::string &src, std::string &fast) // 英文大小写转换服务
{for (auto ch : src){if (ch >= 'a' && ch <= 'z'){fast += ch - 32;}else{fast += ch;}}
}void Proservice(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd)
{char inbuffer[BUFFER];std::string outbuffer;while (true){// 前四步在TcpServer.cc的Start函数中// 5. 读取Client发送的数据 -- TCP->流式socket->文件流IOmemset(inbuffer, 0, BUFFER);ssize_t Read = read(ServerSockfd, inbuffer, BUFFER - 1);if (Read > 0){inbuffer[Read] = 0;// 客户端输入quit时退出本次服务 -- 不区分大小写if (strcasecmp(inbuffer, "quit") == 0){std::cout << "Client[" << ClientPort << ":" << ClientIp<< "]# exit server!!!" << std::endl;break;}std::cout << "[" << ClientPort << ":" << ClientIp << "]echo# "<< inbuffer << std::endl;}else if (Read == 0) // 读端一直读,写端关闭,读端read返回0 -- Read == 0 -> 对方退出{std::cout << "[" << ClientPort << ":" << ClientIp << "]exit! ! !" << std::endl;break;}else{std::cout << "Server read data error" << strerror(errno) << std::endl;break;}// 提供服务 -- 小写英文 -> 大写英文 -- 大小写转换LowToUpp(inbuffer, outbuffer);// 6. 发送数据到Client -- TCP->流式socket->文件流IOssize_t Write = write(ServerSockfd, (void *)outbuffer.c_str(), outbuffer.size());if (Write < 0){std::cout << "Server write data error" << strerror(errno) << std::endl;continue;}outbuffer.clear(); // 清空数据}close(ServerSockfd); // 服务完成后,关闭"服务socket fd"
}
// ---------------------------------------------------------------------------------------------------// ------------------------------------- Client Data -------------------------------------------------
class TcpServer; // 前置声明,不开辟空间class ClientData // 客户端网络数据
{
public:ClientData(const std::string &ClientPort, const std::string &ClientIp, int ServerSockfd, TcpServer *This): ClientPort_(ClientPort),ClientIp_(ClientIp),ServerSockfd_(ServerSockfd),This_(This){}public:std::string ClientPort_;std::string ClientIp_;int ServerSockfd_;TcpServer *This_;
};
// ---------------------------------------------------------------------------------------------------class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): ListenSockfd_(-1), port_(port), ip_(ip){}~TcpServer(){if (ListenSockfd_ > 2){close(ListenSockfd_);}delete ptp;}public:void Init(){// 1. 获取sockfd// SOCK_STREAM: 提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,网络地址族使用TCP协议ListenSockfd_ = socket(AF_INET, SOCK_STREAM, 0);if (ListenSockfd_ < 0){std::cout << "Server make socket error: " << strerror(errno) << std::endl;exit(SOCKET_ERR);}// 2. 绑定主机基本信息到内核struct sockaddr_in Server;socklen_t len = sizeof(Server);memset(&Server, 0, len);Server.sin_family = AF_INET;                                                       // "地址类型/协议家族"Server.sin_port = htons(port_);                                                    // 端口号Server.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); // IP地址if (bind(ListenSockfd_, (const struct sockaddr *)&Server, len) < 0){std::cout << "Server bind error: " << strerror(errno) << std::endl;exit(BIND_ERR);}// 3. 将套接字设置为监听到来的连接请求状态,监听是否有客户到来(TCP -> 面向连接)// 第二个参数:套接字的挂起"连接队列"的最大长度(一般由2到4), 用SOMAXCONN则为系统给出的最大值if (listen(ListenSockfd_, SOMAXCONN) < 0){std::cout << "Server listen error: " << strerror(errno) << std::endl;exit(LISTEN_ERR);}}void Start(){int ServerSockfd;while (true){//-------------------------------------------------------------------------------------// 4. 服务器启动后,获取新的客户端的连接// 该函数返回新的服务套接字,用于服务客户的请求(流式IO服务)struct sockaddr_in Client;socklen_t len = sizeof(Client);ServerSockfd = accept(ListenSockfd_, (struct sockaddr *)&Client, &len);if (ServerSockfd < 0){std::cout << "Server get connection for error" << strerror(errno) << std::endl;continue;}// 获取Client网络信息 -- port、ip address -- 网络序列 <-> 主机序列std::string ClientPort = std::to_string(ntohs(Client.sin_port));std::string ClientIp = inet_ntoa(Client.sin_addr);//-------------------------------------------------------------------------------------//-------------------------------------------------------------------------------------// 线程池版本Task t(ClientPort, ClientIp, ServerSockfd, Proservice); // Proservice是普通函数,不需要bind调整ptp = ThreadPool::GetThreadPool();ptp->push(t);//-------------------------------------------------------------------------------------}}private:int ListenSockfd_; // 监听socket fduint16_t port_;std::string ip_;ThreadPool *ptp;
};// (./tcpserver port [ip])
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){std::cout << "Command Line Format: ./tcpserver port [ip]" << std::endl;exit(FORM_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}Daemon();TcpServer Server(port, ip);Server.Init();Server.Start();return 0;
}