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

UDPTCP网络编程

udp编程接口

一个UDP程序的编写可以分为3步:

  • 创建一个网络套接字:

    它相当于文件操作时的文件描述符,是一个程序进行网络通讯的门户, 所有的网络操作都要基于它

  • 绑定IP和端口:

    需要为网络套接字填充IP和端口信息

    但是有些时候无需手动填充,让系统自动自动分配即可

  • 发送和接收消息

    • 发送消息需要指明对方的IP和端口号
    • 接收消息不需要,直接从套接字拿就行

socket

申请一个套接字

套接字:相当于一个文件描述符,其中存放着IP、端口、网络协议等信息;所有的网络操作都要基于这个网络套接字,就像所有文件操作都要基于文件描述符一样

函数原型及参数解析

#include <sys/socket.h>
#include <sys/types.h> int socket(int domain, int type, int protocol);
  • domain:socket的域;选择本地通讯网络通信

    AF_UNIX(AF_LOCAL):本地通讯

    AF_INET:IPv4协议网络通讯

    AF_INET6:IPv6协议网络通讯

  • type:套接字的类型;决定通信时对应的报文;udp–>用户数据报,tcp–>流式

    SOCK_STREAM:流式–>tcp

    SOCK_DGRAM:数据报格式,无链接,不可靠–>udp

  • protocol:协议类型;网络应用中一般用 0

  • 返回值:返回一个文件描述符

Example

#include <sys/socket.h>
#include <sys/types.h> 
int main()
{int sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){exit(1);}
}

bind

绑定网络信息

将网络信息写入网络套接字对应的内核区域

函数原型及参数解析

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>//struct sockaddr结构体定义
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:网络套接字, 表示将网络信息绑定到这个套接字

  • addr:要进行绑定的网络信息(IP、端口号)

    我们要用一个结构体存储存储网络信息,然后把结构体传入bind函数,用于绑定

    由于socket创建的套接字需要兼容本地、网络等多个域,多个协议,而这些协议需要绑定的信息也不尽相同,对应描述信息的的结构体就不同,如:

    • 对于网络信息的描述就要有IP端口号网络通讯协议(下面的struct sockaddr_in结构体)
    • 本地信息的描述就要有路径名和本地的各种通讯协议(下面的struct sockaddr_un结构体)

    image-20221203161834884

    我们可以用一种多态的理念直接给bind函数传入两种类型结构体变量的首地址,当函数内要获取网络信息的时候,先读前16位知道当前要绑定信息的域和协议

    进而再对后面的位进行特定化读取

    这个addr参数完全可以用一个void*来接收两种不同的结构体指针,但是由于一些历史原因,当时还没有void*的语法

    所以,函数编写者新定义了一个结构体 struct sockaddr

    用法也很简单,只需要把struct sockaddr_in*struct sockaddr_un*强转为 struct sockaddr*传入即可,

    bind函数内部会自动通过通过前16位判断要选择哪种数据类型的绑定

    image-20221128153006434

    sockaddr 结构:

    /* Structure describing a generic socket address.  */
    struct sockaddr{__SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */char sa_data[14];		/* Address data.  */};
    

    sockaddr_in 结构:

    struct sockaddr_in
    {__SOCKADDR_COMMON (sin_);   //16位地址类型,此句相当于unsigned short sin_family;in_port_t sin_port;			//端口号struct in_addr sin_addr;	//IP地址/* Pad to size of `struct sockaddr'.  */unsigned char sin_zero[8];
    };
    struct in_addr
    {unsigned short s_addr;//16位IP地址
    };
    
  • addrlen

    addr结构体变量的大小

Example:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
int main()
{// 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){exit(1);}// 填充网络信息结构体string ip = "127.0.0.1";uint16_t port = 8080; struct sockaddr_in local;bzero(&local, sizeof(local));  // 初始化为全零local.sin_family = AF_INET;    // 填充协议家族,域,与创建套接字时的domain相同local.sin_port = htons(port);  // 填充端口号信息local.sin_addr.s_addr = ip.empty() ? htons(INADDR_ANY) : inet_addr(ip.c_str());// 填充IP信息// 绑定if (bind(sockfd, (sockaddr *)&local, sizeof(local)) < 0){exit(1);}
}

INADDR_ANY:

程序员一般不用关心bind到哪个ip,

INADDR_ANY的值为0,传入的四字节IP如果是INADDR_ANY,则表示让编译器自动选择IP,进行绑定

一般指定填充一个确定的ip,在有特殊用途,或者测试时使用

云服务器上禁止bind的任何确定IP,只能使用 INADDR_ANY

recvfrom

从网络套接字中接收消息

函数原型及参数解析

#include <sys/types.h>
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • sockfd:网络套接字

  • buf:读取到的目标缓冲区,读取长度len

  • flags:等待消息的方式(0–>阻塞式等待)

  • src_addr:发送方的网络信息会被填入其中(输出型参数)

  • 对方网络信息结构体的大小(输入、输出型参数,带入结构体大小,用于说明要为src_addr结构体开辟空间的大小,带出收到结构体大小)

    注意:接收消息时,无需告知发送方的地址,此结构体无需填充,消息会被发送方主动发送过来,通过套接字直接拿取即可

  • 返回值:返回-1表示读取出错

Example

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
int main()
{int sockfd;//生成套接字并完成绑定//...//开始接收消息char inbuff[1024];struct sockaddr_in peer;//用于存放消息发送方的网络信息socklen_t len = sizeof(peer);size_t s = recvfrom(sockfd,inbuff,sizeof(inbuff)-1,0,(sockaddr *)&peer, &len);if (s > 0){inbuff[s] = 0;}else if (s == -1){exit(1);}else;//读取成功,读到了对方的数据和网络地址【IP:port】string ip = inet_ntoa(peer.sin_addr);  //拿到对方的IPuint16_t port = ntohs(peer.sin_port);  //拿到对方的port//打印客户端发过来的消息和网络地址printf("[%s:%d]# %s", ip.c_str(), port, inbuff);return 0;
}

这个程序的功能就是从套接字读取一串字符并打印到屏幕

sendto

发送一条消息

函数原型及参数解析

#include <sys/types.h>
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:网络套接字

  • buf:从目标缓冲区进行读取,读取长度len

  • flags:等待方式,阻塞等待是0(向网络发送消息也要申请一定的资源,是资源就有可能申请不到,就需要提供等待的方式)

  • dest_addr:发送目标的网络信息,

    注意:发送消息一定要通过此结构体,为sendto()提供发送目标的网络信息

  • addrlen:dest_addr结构体的大小

Example

int main(int argc, char const *argv[])
{//获取服务器IP,端口string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);//创建客户端int socketfd = socket(AF_INET, SOCK_DGRAM, 0);struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());string buffer;cout << "请输入:";getline(cin, buffer);//发送消息sendto(socketfd, buffer.c_str(), buffer.size(), 0,(struct sockaddr *)&server, sizeof(server));return 0;
}

IP和port的格式转换

网络字节序<–>本地字节序

​ 由于不同操作系统,不同编译器有不同的字节序,为了网络通信的方便,网络字节序统一规定为大端,

​ 所有要进行网络传输的数据都要先转为网路字节序,

​ 从网络种接收到的数据也要先转为本地字节序

#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);   //32位数转为网络字节序uint16_t htons(uint16_t hostshort);  //16位数转为网络字节序uint32_t ntohl(uint32_t netlong);    //32位数转为本地字节序uint16_t ntohs(uint16_t netshort);   //16位数转为本地字节序

函数名解析:

​ < h: host --> 本地

​ n: network --> 网络

​ l: long --> 32位数

​ s:short --> 16位数 >

点分十进制IP<–>四字节二进制IP

服务器的IP地址我们一般写为:

"xx.xxx.xx.xxx"的点分十进制格式

但是这样的字符串实际不利于存储和计算机运算,所有结构体中存储的IP地址要以位段的方式用一个4字节数表示

如下这些函数用于将点分十进制的IP地址4字节IP互相转换

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//点分十进制字符串-->4字节IP
int inet_aton(const char *cp, struct in_addr *inp); 
// 转换成功返回1,失败返回0,网络字节序ip自动写入in_addr结构体,认为255.255.255.255是有效IP,推荐使用
in_addr_t inet_addr(const char *cp);                
// 返回的4字节数是网络字节序,认为255.255.255.255是无效IP,返回-1
in_addr_t inet_network(const char *cp);             
// 返回的4字节数是本地字节序,认为255.255.255.255是无效IP,返回-1//4字节IP-->点分十进制字符串
char *inet_ntoa(struct in_addr in);

UDP聊天室编写

一个聊天室需要有服务器端和客户端

服务器端负责接收消息,并将收得的消息发送给所有的已登陆用户

客户端负责发送消息,同时接收服务器同步过来的消息

用户登陆方式为:在客户端向服务器发送一条消息

如果用户长时间没有在聊天室发言,将会被提出群聊

日志打印

log.hpp

用于日志信息的打印

#pragma once
#include <stdlib.h>
#include <cassert>
#include <cstdio>
#include <ctime>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);const char* name = getenv("USER");char logInfo[1024];va_list ap;va_start(ap, format);  //让dp对应到可变部分(...)vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap);  // ap = NULLFILE* out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n", log_leval[level],(unsigned)time(nullptr), name == nullptr ? "nukonw" : name,logInfo);
}

服务器端

udpServer.cc

#include <arpa/inet.h>
#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <unordered_map>#include "log.hpp"
using namespace std;class UdpServer
{struct Client{struct sockaddr_in peer;time_t time; // 到time之后如果没有更新过,就清除此用户};private:// 服务器的socket fd信息int sockfd_;// 服务器的端口号信息uint16_t port_;// 服务器IP地址,点分十进制std::string ip_;// 在线用户std::unordered_map<std::string, struct Client> users_;// 超过此时间未响应将被踢出群聊(秒)const int tickOutTime_; public:UdpServer(int port, const string ip = "", int tickOutTime = 1000): sockfd_(-1), // 初始化为-1,如果init创建失败,用-1表示失败port_(port),ip_(ip),tickOutTime_(tickOutTime){}~UdpServer() {}public:void init()//创建套接字并绑定{// 1.创建socked套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);  //相当于打开了一个文件if (sockfd_ < 0){logMssage(FATAL, "%s:%d", strerror(errno), sockfd_);exit(1);}logMssage(DEBUG, "socket create success:%d", sockfd_);// 2. 绑定网络信息,ip+port// 2.1 先填充基本信息到 stckaddr_instruct sockaddr_in local;bzero(&local, sizeof(local));//填充协议家族,域local.sin_family = AF_INET;//填充端口号信息(htons():转为网络字节序)local.sin_port = htons(port_);//服务器都必须有IP地址,"xx.xxx.xx.xxx",// inet_addr():字符串风格点分十进制-->4字节IP-->uint32_t ip(位段方式),// 该函数会自动转网络字节序// INADDR_ANY(0):程序员不关心bind到哪个ip,让编译器自动绑定// inet_addr:指定填充一个确定的ip,特殊用途,或者测试时使用//禁止bind云服务器上的任何确定IP,只能使用 INADDR_ANYlocal.sin_addr.s_addr =ip_.empty() ? htons(INADDR_ANY) : inet_addr(ip_.c_str());// 2.2绑定if (bind(sockfd_, (sockaddr *)&local, sizeof(local)) < 0){logMssage(FATAL, "%s:%d", strerror(errno), sockfd_);exit(2);}}void start(){while (true){// demo2char inbuff[1024];char outbuff[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);size_t s = recvfrom(sockfd_, inbuff, sizeof(inbuff) - 1, 0,(sockaddr *)&peer, &len);if (s > 0){//当作字符串看待inbuff[s] = '\0';  outbuff[s] = '\0';}else if (s == -1){logMssage(WARINING, "recvfrom:%s:%d", strerror(errno), sockfd_);continue;}//读取成功,读到了对方的数据和网络地址【IP:port】string peerIP = inet_ntoa(peer.sin_addr);uint16_t peerPort = ntohs(peer.sin_port);                                     // 拿到对方的portcheckOnlineUser(peerIP, peerPort, {peer, (time_t)time(NULL) + tickOutTime_}); // 如果用户不存在则添加用户,存在则更新时间// 打印客户端发过来的消息和网络地址logMssage(NOTICE, "[%s:%d]# %s", peerIP.c_str(), peerPort, inbuff);messageRoute(peerIP, peerPort, inbuff); // 消息路由(将消息转发给除自己外的所有人)}}private:// 如果用户不存在则添加用户,存在则更新时间void checkOnlineUser(string IP, uint16_t port, const Client &usr){std::string key = IP;key += ":";key += std::to_string(port);if (users_.count(key)){users_[key].time = usr.time; // 更新时间}else{users_.insert({key, usr}); // 添加用户}}// 消息路由(将消息转发给除自己外的所有人)void messageRoute(string IP, uint16_t port, string message){std::string from = IP;from += ":";from += std::to_string(port);string out = "[" + from + "]: " + message;// 记录超时未相应,退出的用户auto it = users_.begin();while (it != users_.end()){auto next = it; // 防止当前节点删除导致迭代器失效next++;if (it->first != from) // 发给自己外的所有人{if (time(NULL) <= it->second.time){sendto(sockfd_, out.c_str(), out.size(), 0, (sockaddr *)&it->second.peer, sizeof(it->second.peer));}else // 用户长时间没有动态将被踢出群聊{// 发送退出消息char exitMessage[] = "\1";sendto(sockfd_, exitMessage, strlen(exitMessage), 0, (sockaddr *)&it->second.peer, sizeof(it->second.peer));auto next = it;users_.erase(it);// exits.push_back(it);}}it = next;}}
};
static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;
}
// 程序运行方式:
// ./udpServer port IP
int main(int argc, char const *argv[])
{//确保命令行参数使用正确if (argc != 2 && argc != 3){Usage(argv[0]);exit(3);}//端口号一定要有uint16_t port = atoi(argv[1]);//IP可以没有string ip;if (argc == 3){ip = argv[2];}//网络服务器UdpServer svr(port, ip);//配置服务器网络信息svr.init();//开始运行服务器svr.start();return 0;
}

客户端

udpClient.cc

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>using namespace std;// 用于接收消息和打印的线程
void *recvAndPrint(void *args)
{char bufferIn[1024]; // 消息接收的缓冲区while (true){int sockfd = *(int *)args;// 从服务器接收消息struct sockaddr_in temp;// temp无需填充,作为输出型参数,带出服务器网络信息socklen_t len = sizeof(temp);// 接收消息不需要提供目的地址(网络信息),消息会被目标主动发送到本地套接字size_t s = recvfrom(sockfd, bufferIn, sizeof(bufferIn) - 1, 0, (struct sockaddr *)&temp, &len);if (s > 0){bufferIn[s] = 0;if (bufferIn[0] == '\1'){cout << "\r长时间未响应,你已退出群聊\n";exit(0);}cout << "\rserver echo# " << bufferIn << endl;cout << "请输入:";}}
}static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << "IP port" << std::endl;
}int main(int argc, char const *argv[])
{// 必须有传入IP 和 端口号if (argc != 3){Usage(argv[0]);exit(1);}// 1.创建客户端int sockfd = socket(AF_INET, SOCK_DGRAM, 0);// 不需要手动绑定,让系统自动为客户端分配IP和端口// 2.通讯过程// 2.1创建线程,循环从网络套接字接收消息pthread_t t;pthread_create(&t, NULL, recvAndPrint, &sockfd);// 2.2发送消息// 配置服务器的网络信息——发送消息的目的地// 从命令行参数获取服务器IP,端口string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 填写服务器的网络信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 发送内容的缓冲区string bufferOut;// 循环读取内容并发送给服务器while (true){cout << "请输入:";getline(cin, bufferOut);//发送消息给serversendto(sockfd, bufferOut.c_str(), bufferOut.size(), 0,(struct sockaddr *)&server, sizeof(server));}return 0;
}

makefile

.PHONY:all
all:udpClient udpServerudpClient:udpClient.ccg++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f udpClient udpServer

tcp编程接口

tcp编程的前两步也依然是

  1. 创建socket套接字
  2. 绑定网络信息

与udp的区别在于:

创建socket的时候type选择SOCK_STREAM,的流式套接字

如下为tcp独有的部分:

  1. 设置为监听状态(listen)

    此步骤不一定要进行,只有被连接一方(服务器)需要设为监听

  2. 获取连接(accept)/发起连接(connect)

    一般由客户端发起连接(客户端知道服务器的IP和port),服务器获取连接

  3. 进行发消息(write)和读消息(read)

一般程序分为服务器端和客户端,编写步骤如下:

服务器端:

创建套接字
(socket)
绑定服务器信息
(bind)
设置监听状态
(listen)
获取连接
(accept)
发送/接收消息
(write/read)

客户端:

创建套接字
(socket)
发起连接请求
(accept)
发送/接收消息
(write/read)

listen

将socket套接字设为监听状态

因为tcp是面向面向连接的,所以要把当前套接字设为可连接的状态

函数原型及参数解析

#include <sys/types.h>          
#include <sys/socket.h>int listen(int sockfd, int backlog);
  • sockfd:网络套接字
  • backlog:完成lisen后,当对端向本地发起connect,操作系统会自动完成连接,此连接被放入一个队列,当本地调用accept,就会从此队列中拿取连接状态,backlog即表示对列的最大容量,当超过此容量,操作系统即不会与对端进行连接(后续TCP原理时会详细讲解)
  • 返回值:成功返回0,失败返回-1,同时设置errno

example

如下是从创建套接字到设置监听状态的三个步骤:

int main()
{// 创建套接字int listenSock_ = socket(AF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){exit(1);}// bind// 2.1填充服务器信息uint16_t port_ = 8080;string ip_;struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2绑定if (bind(listenSock_, (const struct sockaddr *)&local, sizeof(local)) == -1){exit(2);}// 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态if (listen(listenSock_, 5) < 0){exit(3);}
}

accept

让处于监听状态的套接字获取连接,此时如果对端发起connect即可完成连接。

函数原型及参数解析

#include <sys/types.h>  
#include <sys/socket.h>	int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:监听套接字;这个socket绑定了IP和port,且设置了监听状态,通过它即可获取远端发起的连接
  • addr:发起连接一方的网络信息会被填入其中(输出型参数)
  • addrlen:传入addr对应结构体的大小,对端返回的结构体大小会被这个参数带出(输入输出型参数)
  • 返回值:返回一个新的文件描述符(套接字),后续的读写操作都要通过这个文件描述符进行

两个套接字的区别:

  • 传入的套接字是监听套接字,一般只有一个,这个套接字设置了监听状态,专门用于accept各个客户端发来的connect请求,让本地和对端建立连接
  • 返回的套接字可以认为是建立连接后,和远端对话的接口,如果有N个客户端和本地建立连接,则有N的服务套接字生成

example

如下代码续接listen的example:

struct sockaddr_in peer;
socklen_t size = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{// 获取连接失败cerr << "accept error";
}

connect

向服务器发起连接

函数原型及参数解析

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

一般由客户端发起连接,创建套接字之后无需手动绑定无需设置监听状态,直接向远端发起connect,即可和远端服务器建立连接,在此过程,系统会自动为这个套接字绑定IP和端口,同时自己的网络信息也会被发送到远端。

  • sockfd:网络套接字
  • addr:对端的网络信息结构体,需要提前创建此结构体并填充IP,port,协议等信息
  • addr对应结构体的大小
  • 返回值:成功返回0;失败返回-1,并设置errno

example

int main()
{// 1.创建客户端套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);// ?2.不需要手动绑定,让系统自动为客户端分配IP和端口// ?3.不需要listen// 2.connect,向服务器发起连接请求std::string server_ip = "127.0.0.1";uint16_t server_port = atoi(8080);// 2.1 先填充远端的主机信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;                    // 协议server.sin_port = htons(server_port);           // portinet_aton(server_ip.c_str(), &server.sin_addr); // ip// 2.2发起请求,connect会自动选择合适的IP和port进行bindif (connect(sockfd, (const struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "connect: " << strerror(errno);exit(CONN_ERR);}
}

read

接收网络消息与文件读取用的是同一个函数

函数原型及参数解析

#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
  • fd:如果是读取本地的文件,fd是open()打开文件后生成的文件描述符;
    如果是接收网络消息,fd则是网络套接字,注意这个套接字不能是监听套接字,
    可以是客户端没有设置过listen的套接字,也可以是服务器端accept返回的套接字
  • buf:缓冲区,读到的信息存于此处
  • count:表示要读取的字节数
  • return:读到的字节数,对端退出返回0,读取失败返回-1

example

如下是server端代码,续接accept的example:

int main()
{// 1.创建套接字// 2.绑定// 3.设置监听状态// 4.获取连接// 5.读取对端发来的消息char inbuffer[BUFFER_SIZE];ssize_t s = read(serviceSock, inbuffer, sizeof(inbuffer) - 1);if (s > 0) // 读到的字节数{// read成功inbuffer[s] = '\0';std::cout << inbuffer << std::endl;}
}

write

向网络发送消息和写文件使用同一个API

函数原型及参数解析

#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
  • fd:网络套接字,用法对标read
  • buf:缓冲区,将缓冲区的内容发出
  • count:需要发送的字节数
  • 返回值:读到的字节数,对端退出返回0,发送失败返回-1

example

续接read的example:

int main()
{// 1.创建套接字// 2.绑定// 3.设置监听状态// 4.获取连接// 5.读取对端发来的消息char inbuffer[BUFFER_SIZE];ssize_t s = read(serviceSock, inbuffer, sizeof(inbuffer) - 1);if (s > 0) // 读到的字节数{// read成功inbuffer[s] = '\0';for (auto &e : inbuffer){e = toupper(e);}write(listenSock_, inbuffer, s);}
}

将接收到的所有字符转为大写并发回

Tcp程序编写—字符转大写

如下是分别是客户端和服务器程序的源代码,服务器会将客户端发送过来的所有消息转为大写后发回,服务意义不大,旨在理解Tcp套接字的使用

工具包

util.h

#pragma once
#include "log.hpp"
#include <arpa/inet.h>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <signal.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024

服务器端

tcpServer.cc

#include "util.hpp"
class TcpServer
{struct ThreadData{int sock_;uint16_t clientPort_;std::string clientIp_;TcpServer *this_;};private:// sockint listenSock_;// portuint16_t port_;// ipstd::string ip_;public:TcpServer(uint16_t port, std::string ip = ""): listenSock_(-1),port_(port),ip_(ip){}void init(){// 创建套接字listenSock_ = socket(AF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){logMssage(FATAL, "socket:%s", strerror(errno));exit(SOCKET_ERR);}logMssage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);// bind// 2.1填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2绑定if (bind(listenSock_, (const struct sockaddr *)&local, sizeof(local)) == -1){logMssage(FATAL, "bind:%s", strerror(errno));exit(BIND_ERR);}logMssage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);// 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态if (listen(listenSock_, 5) < 0){logMssage(FATAL, "listen:%s", strerror(errno));exit(LISTEN_ERR);}logMssage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);}void loop(){while (true){// 4.获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取连接失败logMssage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);continue;}// 获取客户端的基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMssage(DEBUG, "addept:%s | %s[%d]", strerror(errno), peerIp.c_str(), peerPort);// 5.提供服务,小写-->大写
#if 0// 5.1 v1版本,单进程版本,一旦进入,无法向后执行,同一时间只能为一个用户提供服务transService(serviceSock, peerIp, peerPort);
#elif 0// 5.2 v2.1版本,多进程,每个用户占据一个子进程signal(SIGCHLD, SIG_IGN);pid_t id = fork();assert(id != -1);if (id == 0){// 子进车close(listenSock_);transService(serviceSock, peerIp, peerPort);exit(0); // 进入僵尸}// 父进程close(serviceSock); // 子进程关不了父进程的// 可以非阻塞式等待,但比较复杂// 可以signal忽略SIGCHILD信号
#elif 0// 5.2 v2.2版本,多进程,创造孤儿进程,无需忽略SIGCHILDpid_t id = fork();assert(id != -1);if (id == 0){close(listenSock_);// 子进车if (fork() > 0)exit(0); // 退出子进程// 孙子进程成为孤儿进程,由系统领养--回收问题由系统解决// 让孙子进程完成任务transService(serviceSock, peerIp, peerPort);exit(0); // 孙子进程退出}// 父进程close(serviceSock);                  // 孙子进程关不了父进程的pid_t ret = waitpid(id, nullptr, 0); // 回收子进程assert(ret > 0);
#else// 5.3 v3 多线程版本// 为线程提供的网络信息ThreadData *threadData = new ThreadData();threadData->clientIp_ = peerIp;threadData->clientPort_ = peerPort;threadData->sock_ = serviceSock;threadData->this_ = this;pthread_t tid;if (pthread_create(&tid, NULL, threadRoutine, threadData) < 0){logMssage(WARINING, "pthread_create:%s", strerror(errno));}
#endif// debug//  logMssage(DEBUG, "server 开始提供服务...");//  sleep(1);}}static void *threadRoutine(void *args){pthread_detach(pthread_self()); // 设置线程分离,无需主线程joinThreadData *td = static_cast<ThreadData *>(args);td->this_->transService(td->sock_, td->clientIp_, td->clientPort_);delete td;}// 将所有的的字母转为大写void transService(int sock, const std::string &clientIP, uint16_t clientPort){assert(sock >= 0);assert(!clientIP.empty());assert(clientPort >= 1024);char inbuffer[BUFFER_SIZE];while (true){ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);if (s > 0) // 读到的字节数{// read成功inbuffer[s] = '\0';if (strcasecmp(inbuffer, "quit") == 0){logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);break;}// 进行大小写转化logMssage(DEBUG, "trans befor: %s", inbuffer);for (int i = 0; i < s; i++){if (isalpha(inbuffer[i]) && islower(inbuffer[i])){inbuffer[i] = toupper(inbuffer[i]);}}logMssage(DEBUG, "trans after: %s", inbuffer);write(sock, inbuffer, strlen(inbuffer));}else if (s == 0){// 代表对方关闭,client退出logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);break;}else{// 读取出错logMssage(WARINING, "%s[%d] -- read:%s", clientIP.c_str(), clientPort, strerror(errno));break;}}// client退出,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有归还,会造成文件描述符溢出logMssage(DEBUG, "server close %d done", sock);}
};static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;std::cout << "example:\n\t" << porc << " 8080 127.0.0.1" << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}TcpServer srv(port, ip);srv.init();srv.loop();return 0;
}

客户端

tcpClient.cc

#include "util.hpp"
static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << "IP port" << std::endl;
}
volatile static bool quit = false;
int main(int argc, char const *argv[])
{// 必须有传入IP 和 端口号if (argc != 3){Usage(argv[0]);exit(1);}// 1.创建客户端int sockfd = socket(AF_INET, SOCK_STREAM, 0);// ?2.不需要手动绑定,让系统自动为客户端分配IP和端口// ?3.不需要listen// ?4.不需要accept// 2.connect,向服务器发起连接请求// 从命令行参数获取服务器IP,端口std::string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);// 2.1 先填充远端的主机信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;                    // 协议server.sin_port = htons(server_port);           // portinet_aton(server_ip.c_str(), &server.sin_addr); // ip// 2.2发起请求,connect会自动选择合适的IP和port进行bindif (connect(sockfd, (const struct sockaddr *)&server, sizeof(server)) != 0){std::cerr << "connect: " << strerror(errno);exit(CONN_ERR);}std::cout << "info: connect success" << sockfd << std::endl;while (!quit){std::string message;std::cout << "请输入:";std::getline(std::cin, message);if (strcasecmp(message.c_str(), "quit") == 0){quit = true;}ssize_t s = write(sockfd, message.c_str(), message.size());if (s > 0){message.resize(1024);ssize_t s = read(sockfd, (char *)(message.c_str()), 1024);std::cout << "Server Echo>>>" << message << std::endl;}else if (s <= 0){break;}}close(sockfd);return 0;
}

TCP服务器(线程池版本)

上面的字符转换服务器我们分别尝试了单执行流、多进程、多线程的版本

单执行流同一时间只能对一个客户端进行服务,只有该客户端退出才能对下一个客户端进行服务

多线程和多进程的版本使用n个线程或进程同时对n个客户进行服务

多线程因为粒度更低,调用成本相对较低

但是,它们都是在完成网络连接之后,再为客户端现场新建一个线程/进程

我们不妨使用一个线程池,让服务器刚启动的时候就创建一些线程,一旦连接成功,直接可以交给线程池执行服务

为了提高趣味性,我们再改一下服务器提供的服务:使用popen()这个系统调用,让客户端可以向服务器发送一些命令让服务器执行,同时返回执行结果,如:客户端发送ls指令,服务器端便会发回它当前目录的文件

tcpServer.cc//tcp服务器源代码

#include "util.hpp"
class TcpServer
{
private:// sockint listenSock_;// portuint16_t port_;// ipstd::string ip_;ThreadPool<Task> *tp_;public:TcpServer(uint16_t port, std::string ip = ""): listenSock_(-1),port_(port),ip_(ip),tp_(nullptr){}void init(){// 创建套接字listenSock_ = socket(AF_INET, SOCK_STREAM, 0);if (listenSock_ < 0){logMssage(FATAL, "socket:%s", strerror(errno));exit(SOCKET_ERR);}logMssage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);// bind// 2.1填充服务器信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = PF_INET;local.sin_port = htons(port_);ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));// 2.2绑定if (bind(listenSock_, (const struct sockaddr *)&local, sizeof(local)) == -1){logMssage(FATAL, "bind:%s", strerror(errno));exit(BIND_ERR);}logMssage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);// 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态if (listen(listenSock_, 5) < 0){logMssage(FATAL, "listen:%s", strerror(errno));exit(LISTEN_ERR);}logMssage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);// 加载线程池tp_ = ThreadPool<Task>::getInstance();}void loop(){tp_->start(); // 启动线程池logMssage(DEBUG, "thread pool start success, thread num:%d", tp_->threadNum());while (true){// 4.获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取连接失败logMssage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);continue;}// 获取客户端的基本信息uint16_t peerPort = ntohs(peer.sin_port);std::string peerIp = inet_ntoa(peer.sin_addr);logMssage(DEBUG, "addept:%s | %s[%d]", strerror(errno), peerIp.c_str(), peerPort);// 5.提供服务,线程池版本 Task t(serviceSock, peerIp, peerPort, std::bind(&TcpServer::execCommand, this, placeholders::_1, placeholders::_2, placeholders::_3));// bind: (this,sock,ip,port)-->(sock,ip,port)// C++11语法,详见包装器一文tp_->push(t); // 传入任务}}void execCommand(int sock, const std::string &clientIP, uint16_t clientPort)//调用popen完成对端发来的指令(循环接收,直到客户退出,断开连接){assert(sock >= 0);assert(!clientIP.empty());assert(clientPort >= 1024);char command[BUFFER_SIZE];while (true){ssize_t s = read(sock, command, sizeof(command) - 1);if (s > 0) // 读到的字节数{command[s] = '\0';logMssage(DEBUG, "[%s:%d] exec [%s]", clientIP.c_str(), clientPort, command);FILE *fp = popen(command, "r");if (fp == nullptr){logMssage(WARINING, "exec %s failed, beacuse:%s", command, strerror((errno)));break;}// dup2(sock, fp->_fileno);//错误,注意区分文件读和写缓冲区// fflush(fp);char line[1024];while (fgets(line, sizeof(line), fp) != nullptr){write(sock, line, strlen(line));}pclose(fp);logMssage(DEBUG, "[%s:%d] exec [%s] ... done", clientIP.c_str(), clientPort, command);}else if (s == 0){// 代表对方关闭,client退出logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);break;}else{// 读取出错logMssage(WARINING, "%s[%d] -- read:%s", clientIP.c_str(), clientPort, strerror(errno));break;}}// client退出,服务到此结束close(sock); // 如果一个进程对应的文件fd,打开了没有归还,会造成文件描述符溢出logMssage(DEBUG, "server close %d done", sock);}
};static void Usage(const std::string porc)
{std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;std::cout << "example:\n\t" << porc << " 8080 127.0.0.1" << std::endl;
}
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}TcpServer srv(port, ip);srv.init();srv.loop();return 0;
}

threadPool.hpp//具体线程池的编写可以看线程控制一文

#pragma once
#include "Lock.hpp"
#include <assert.h>
#include <iostream>
#include <pthread.h>
#include <queue>
#include <stdlib.h>
#include <sys/prctl.h> //更改线程名,便于调试查看
#include <unistd.h>
using namespace std;const int gThreadNum = 5;
template <class T>
class ThreadPool
{
private:bool isStart;           // 判断防止当前线程池多次被启动int threadNum_;         // 线程的数量queue<T> taskQueue_;    // 任务队列pthread_mutex_t mutex_; // 保证访问任务队列是原子的pthread_cond_t cond_;   // 如果当前任务队列为空,让线程等待被唤醒bool quit_;static ThreadPool<T> *instance_; // 设计成单例模式public:static ThreadPool<T> *getInstance(){static Mutex mutex;if (nullptr == instance_) // 仅仅过滤重复的判断{Lock_Guard lockGuard(&mutex); // 保护后面的内容if (nullptr == instance_){instance_ = new ThreadPool<T>();}}return instance_;}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}public:void start() // 创建多个线程,让它们等待被唤醒,执行push的任务{assert(isStart == false);isStart = true;for (int i = 0; i < threadNum_; i++){pthread_t tmp;pthread_create(&tmp, nullptr, threadRoutine, this);}}void quit() // 关闭线程池时确保所有任务都完成了{while (haveTask()){pthread_cond_broadcast(&cond_);// usleep(1000);//  cout << taskQueue_.size() << endl;}quit_ = true;}void push(const T &in) // 在线程池中添加任务{lockQueue();taskQueue_.push(in);choiceThreadForHandl();unlockQueue();}int threadNum(){return threadNum_;}private:ThreadPool(int threadNum = gThreadNum){threadNum_ = threadNum;assert(threadNum > 0);isStart = false;quit_ = false;pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;           // 单例防拷贝ThreadPool operator=(const ThreadPool<T> &) = delete; // 同上static void *threadRoutine(void *args){prctl(PR_SET_NAME, "follower");pthread_detach(pthread_self());ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (true) // 循环从任务队列中拿出任务并执行,队列为空则等待任务出现{tp->lockQueue();while (!tp->haveTask()) // 如果任务队列为空则等待{if (tp->quit_) // 当调用quit且队列已经为空的时候quit_才会被置为true{cout << "quit" << endl;return nullptr;}tp->waitForTask();}// 将任务从队列中拿到出来执行T t = tp->pop();tp->unlockQueue();t();// 规定所有任务内都有一个自己的run方法}return nullptr;}void lockQueue() // 加锁{pthread_mutex_lock(&mutex_);}void unlockQueue() // 解锁{pthread_mutex_unlock(&mutex_);}void waitForTask() // 让线程等待被唤醒{pthread_cond_wait(&cond_, &mutex_);}bool haveTask() // 队列不为空{return !taskQueue_.empty();}void choiceThreadForHandl() // 随便唤醒一个等待的线程{pthread_cond_signal(&cond_);}T pop() // 从队列中拿取一个任务{T tmp = taskQueue_.front();taskQueue_.pop();return tmp;}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance_ = nullptr; // 单例

Task.hpp//提供任务类,可使用回调的方法给线程池传入任务

#pragma once
#include "log.hpp"
#include <functional>
#include <iostream>
#include <string>
class Task
{using callback_t = std::function<void(int, std::string, uint16_t)>;// 等价于std::function<void(int, std::string, uint16_t)> callback_t;
private:int sock_;std::string ip_;uint16_t port_;callback_t func_;
public:Task() : sock_(-1), port_(-1) {}Task(int sock, std::string ip, uint16_t port, callback_t func): sock_(sock), ip_(ip), port_(port), func_(func){}void operator()(){logMssage(DEBUG, "线程ID[%p]处理%s:%d的请求开始了...", pthread_self(), ip_.c_str(), port_);func_(sock_, ip_, port_);logMssage(DEBUG, "线程ID[%p]处理%s:%d的请求完成了...", pthread_self(), ip_.c_str(), port_);}
};

Lock.hpp//封装了互斥锁、设计了RAII的LockGard,如果熟悉C++线程库,可以直接使用C++线程库

#pragma once
#include <iostream>
#include <pthread.h>class Mutex
{
private:pthread_mutex_t lock_;public:Mutex(){pthread_mutex_init(&lock_, nullptr);}~Mutex(){pthread_mutex_destroy(&lock_);}void lock(){pthread_mutex_lock(&lock_);}void unlock(){pthread_mutex_unlock(&lock_);}
};class Lock_Guard
{
private:Mutex *mutex_;public:Lock_Guard(Mutex *mutex): mutex_(mutex){mutex_->lock();}~Lock_Guard(){mutex_->unlock();}
};

log.hpp//提供日志函数,方便打印详细的日志信息

#pragma once#include <stdlib.h>#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <ctime>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);const char* name = getenv("USER");char logInfo[1024];va_list ap;va_start(ap, format);  //让dp对应到可变部分(...)vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap);  // ap = NULLFILE* out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n", log_leval[level],(unsigned)time(NULL), name == NULL ? "nukonw" : name,logInfo);
}

util.hpp//工具包:包含了所有要包含的头文件和一些宏定义

#pragma once
#include "Lock.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "log.hpp"
#include <arpa/inet.h>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <signal.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024

将服务器端“守护进程”化

一般服务器进程都是以守护进程的形式出现的(具体守护进程的概念,见《[进程概念](#(572条消息) 进程概念(Linux)_Massachusetts_11的博客-CSDN博客)》->守护进程),一旦启动之后,除非用户主动关闭,否则一直会在运行

setid()可以更改当前进程的会话ID

但是调用此函数的进程不能是一个进程的组长

所以,一般我们需要fork()一个子进程,让子进程setsid,父进程可以直接exit();

if(fork() > 0) exit(0);
setsid(1);

除了守护进程化,一般服务器程序还要进行一些选做内容

  • 忽略SIGPIPE信号

    如果server端在write时,Client已经退出,则server端也会被SIGPIPE信号终止,所以我们要忽略此信号

  • 更改进程的工作目录:

    chdir();//《进程控制》一文可以看到

  • 删除/修改0,1,2号文件描述符

    因为一般服务器端不会在标准输入输出流进行输入输出

    所以我们可以将0,1,2号文件描述符关掉,但是很少有人这么做

    在Linux下有一个“垃圾桶”或者说“文件黑洞”,

    凡是写入/dev/null中的数据,一概会被丢弃,从中读取也是空

    所以,我们可以打开 /dev/null,并且对0,1,2进行重定向

    或者也可以创建一个日志文件,将产生的日志信息存储到文件中去

daemaonize.hpp

#pragma once#include <cstdio>
#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
void daemonize() // 将程序守护进程化
{// 1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);// 2. 更改进程的工作目录// chdir();// 3. 让自己不要成为进程组组长if (fork() > 0)exit(0);// 4.设置自己时一个独立的会话setsid();// 5. 重定向0,1,2int fd = 0;if ((fd = open("/dev/null", O_RDWR) != -1)){dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 关闭掉不需要的fdif (fd > STDERR_FILENO)close(fd);}
}

log.hpp

#pragma once#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
// 日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define LOGFILE "tcpServer.log"const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);const char* name = getenv("USER");char logInfo[1024];va_list ap;va_start(ap, format);  //让dp对应到可变部分(...)vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap);  // ap = NULLint fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);assert(fd > 0);FILE *out = (level == FATAL) ? stderr : stdout;dup2(fd, 1);dup2(fd, 2);fprintf(out, "%s | %u | %s | %s\n", log_leval[level],(unsigned)time(NULL), name == NULL ? "nukonw" : name,logInfo);fflush(out); // 将C缓冲区的数据刷新到OSfsync(fd);   // 将OS中的数据尽快刷盘
}

只需要在服务器端的main函数调用daemonize()即可完成守护进程化

tcpServer.cc

int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::string ip;if (argc == 3){ip = argv[2];}daemonize(); // 我们的进程将成为守护进程TcpServer srv(port, ip);srv.init();srv.loop();return 0;
}

一般守护进程化的程序结尾带一个d

makefile

.PHONY:all
all:tcpClient tcpServerdtcpClient:tcpClient.ccg++ -o $@ $^ -std=c++11 -lpthread
tcpServerd:tcpServer.ccg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -f tcpClient tcpServerd

此时,我们的Tcp服务器就成为了守护进程

相关文章:

UDPTCP网络编程

udp编程接口 一个UDP程序的编写可以分为3步&#xff1a; 创建一个网络套接字&#xff1a; 它相当于文件操作时的文件描述符&#xff0c;是一个程序进行网络通讯的门户&#xff0c; 所有的网络操作都要基于它 绑定IP和端口&#xff1a; 需要为网络套接字填充IP和端口信息 但是…...

【微信小程序】-- 全局配置 -- tabBar(十七)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &…...

Cortex-A7中断控制器GIC

Cortex-A7中断控制器GIC 中断号 芯片内部的中断都会引起IRQ InterruptGIC将所有的中断源(最多1020个中断ID)分为三类: SPI(SharedPeripheralInterrupt)共享中断&#xff0c;外部中断都属于SPI中断 [ID32-1019]PPI(PrivatePeripheralInterrupt)私有中断 [ID16-31]SGI(Software-…...

JavaSE:常用类

前言从现在开始进入高级部分的学习&#xff0c;鼓励自己一下&#xff01;画个大饼&#xff1a; 常用类->集合框架->IO流->多线程->网络编程 ->注解与反射->GUI很重要的东西&#xff0c;不能不会&#xff01;Object类祖宗类&#xff0c;主要方法&#xff1a;t…...

Element中树形控件在项目中的实际应用

文章目录1、使用目的2、官网组件3、组合使用组件案例4、在项目中实际应用4.1 组合组件的使用4.1.2 代码落地4.1.3 后台接口数据4.1.4 实际效果官网连接直达&#xff1a;Tree树形控件的使用 1、使用目的 用清晰的层级结构展示信息&#xff0c;可展开或折叠。 2、官网组件 <…...

kaggle RSNA 比赛过程总结

引言 算算时间&#xff0c;有差不多两年多没在打kaggle了&#xff0c;自20年最后一场后&#xff08;其实之前也就打过两场&#xff0c;一场打铁&#xff0c;一场表格赛是金是银不太记得&#xff0c;当时相当于刺激战场&#xff0c;过拟合lb大赛太刺激了&#xff0c;各种trick只…...

51单片机入门————LED灯的控制

LED的电路图通过原理图看出&#xff0c;LED灯是接单片机芯片的P20~P27的一共有8个LED&#xff0c;51单片机也是8字节的P20x010xFE————1111 1110P20xFE可以表示把在P2端的第一个灯点亮1 表示高电平0表示低电平当为0的时候形成一个完整回路&#xff0c;电流从高电平流向低电平…...

J - 二进制与、平方和(线段树 + 维护区间1的个数)

2023河南省赛组队训练赛&#xff08;二&#xff09; - Virtual Judge (vjudge.net) 请你维护一个长度为 n 的非负整数序列 a1, a2, ..., an&#xff0c;支持以下两种操作&#xff1a; 第一种操作会将序列 al, al  1, ..., ar 中的每个元素&#xff0c;修改为各自和 x…...

BertTokenizer的使用方法(超详细)

导入 from transformers import BertTokenizer from pytorch_pretrained import BertTokenizer以上两行代码都可以导入BerBertTokenizer,transformers是当下比较成熟的库&#xff0c;pytorch_pretrained是google提供的源码(功能不如transformers全面) 加载 tokenizer BertT…...

深度学习编译器CINN(3):编译过程中遇到的问题总结

目录 问题一:No module named XXXX 问题描述 分析与解决方案 问题二:catastrophic error: cannot open source file "float16.h"...

yum 安装mysql8数据全过程

mysql8安装方式&#xff1a;&#xff08;使用官方yum仓库&#xff09; 1. wget https://dev.mysql.com/get/mysql80-community-release-el7-4.noarch.rpm 安装 yum install mysql80-community-release-el7-4.noarch.rpm 2、生成yum源缓存 每次当我们编写了&#xff0c…...

内网vCenter部署教程一

PS:因为交换机链路为trunk,安装先登录ESXI,将端口组改为管理vlan ID(1021) 一、双击镜像,打开文件夹,目录为F:\vcsa-ui-installer\win32,双击installer.exe 二、先设置语言为中文 三、点击下一步 四、选择需要安装esxi的主机。 五、设置Vcenter虚拟机的密码...

java 进阶—线程的常用方法

大家好&#xff0c;通过java进阶—多线程&#xff0c;我们知道的什么是进程&#xff0c;什么是线程&#xff0c;以及线程的三种创建方式的选择 今天&#xff0c;我们来看看线程的基础操作 start() 开启线程 public class Demo implements Runnable {Overridepublic void run…...

hadoop的运行模式

作者简介&#xff1a;大家好我是小唐同学(๑>؂<๑&#xff09;&#xff0c;好久不见&#xff0c;为梦想而努力的小唐又回来了&#xff0c;让我们一起加油&#xff01;&#xff01;&#xff01; 个人主页&#xff1a;小唐同学(๑>؂<๑&#xff09;的博客主页 目前…...

服务器(centos7.6)已经安装了宝塔面板,想在里面安装一个SVN工具(subversion),应该如何操作呢?

首先&#xff0c;在登录进入宝塔面板&#xff0c;然后点击左侧终端&#xff0c;进入终端界面&#xff0c;如下图&#xff1a;------------------------------------------如果是第一次使用会弹出输入服务器用户名和密码&#xff0c;此时输入root账号和密码&#xff0c;即可进入…...

从智能进化模型看用友BIP的AI平台化能力

随着人工成本的上升&#xff0c;智能和自动化技术的成熟&#xff0c;企业在越来越多的场景开始应用自动化技术来替代相对标准及有规则的工作&#xff0c;同时利用智能算法来优化复杂工作及决策&#xff0c;获得竞争优势。 不同于阅读、聊天、搜索等面向终端用户的应用场景&…...

项目管理的主要内容包括哪些?盘点好用的项目管理系统软件

阅读本文您将了解&#xff1a;1、项目管理的主要内容包括哪些2、好用的项目管理软件 项目管理是为了实施一个特定目标&#xff0c;所实施的一系列针对项目要素的管理过程&#xff0c;包括过程、手段以及技术等。 通过项目管理&#xff0c;我们能够提前安排和控制项目的时间、…...

Allegro如何查看PCB上器件的库路径操作指导

Allegro如何查看PCB上器件的库路径操作指导 在做PCB设计的时候,有时需要检查PCB上器件使用的库的路径是否正确,Allegro支持快速将PCB上所有器件的库路径都列出来 如下图 如何显示这个报表,具体操作如下 点击Tools点击Report...

笔记【尚硅谷】大数据Canal教程丨Alibaba数据实时同步神器

视频教程&#xff1a;【尚硅谷】大数据Canal教程丨Alibaba数据实时同步神器教程资料&#xff1a;https://pan.baidu.com/s/1VhGBcqeywM6jyXJxtytd1w?pwd6666&#xff0c;提取码&#xff1a;6666本套教程以Canal的底层原理展开讲解&#xff0c;细致地介绍了Canal的安装部署及常…...

如何重定向命令行日志信息到指定txt文件?

如果你想把命令行的输出重定向到指定的txt文件&#xff0c;你可以使用一些符号来实现。例如&#xff0c;你可以在命令后面加上>或>>符号&#xff0c;然后指定文件名。例如&#xff1a; command > output.txt 这样就会把command的标准输出保存到output.txt文件中&…...

物理机不能访问虚拟机kali的web服务解决方案记录

目录 环境 问题描述 解决方案 知识补充 效果测试 其他思路 环境 kali&#xff08;nat模式&#xff09;&#xff0c;物理机&#xff0c;可互ping 问题描述 kali的web服务器不能在物理机上访问。 1.本机能ping通虚拟机 2.虚拟机也能ping通本机 3.虚拟机能访问自己的web …...

服务器配置 | 在Windows本地显示远程服务器绘图程序

文章目录方法1&#xff1a;在MobaXterm的终端输入指令方法2&#xff1a;在Pycharm中运行前提概要&#xff0c;需要在本地Windows端显示点云的3d可视化界面 对于点云的3d可视化一般有两种方法&#xff0c;open3d显示或者是mayavi显示。这两个库都可以使用pip install来实现安装…...

高级信息系统项目管理(高项 软考)原创论文——质量管理(2)

<...

从0开始学python -47

Python CGI编程 -2 GET和POST方法 浏览器客户端通过两种方法向服务器传递信息&#xff0c;这两种方法就是 GET 方法和 POST 方法。 使用GET方法传输数据 GET方法发送编码后的用户信息到服务端&#xff0c;数据信息包含在请求页面的URL上&#xff0c;以"?"号分割…...

【数据结构】八大经典排序总结

文章目录一、排序的概念及其运用1.排序的概念2.常见排序的分类3.排序的运用二、常见排序算法的实现1.直接插入排序1.1排序思想1.2代码实现1.3复杂度及稳定性1.4特性总结2.希尔排序2.1排序思想2.3复杂度及稳定性2.4特性总结3.直接选择排序3.1排序思想3.2代码实现3.3复杂度及稳定…...

BI的能力边界:能解决的企业问题和不擅长的领域

数字化转型本就需要借助信息化相关技术、思想来完成&#xff0c;所以说信息化建设同样是数字化转型过程中非常重要的一环&#xff0c;而这就是商业智能BI和数字化转型的关系 BI 能解决的企业问题 数据是企业的重要资产&#xff0c;也是企业商业智能BI的核心要求。通常&#x…...

金三银四面试必备,“全新”突击真题宝典,阿里腾讯字节都稳了

前言招聘旺季就到了&#xff0c;不知道大家是否准备好了&#xff0c;面对金三银四的招聘旺季&#xff0c;如果没有精心准备那笔者认为那是对自己不负责任&#xff1b;就我们Java程序员来说&#xff0c;多数的公司总体上面试都是以自我介绍项目介绍项目细节/难点提问基础知识点考…...

MYSQL 基础篇 | 02-MYSQL基础应用

文章目录1 MySQL概述2 SQL2.1 SQL通用语法2.2 SQL分类2.3 DDL2.3.1 数据库操作2.3.2 表操作2.4 DML2.4.1 添加数据2.4.2 修改数据2.4.3 删除数据2.5 DQL2.5.1 基础查询2.5.2 条件查询2.5.3 聚合查询2.5.4 分组查询2.5.5 排序查询2.5.6 分页查询2.5.7 综合练习2.6 DCL2.6.1 管理…...

CSS实现checkbox选中动画

前言 &#x1f44f;CSS实现checkbox选中动画&#xff0c;速速来Get吧~ &#x1f947;文末分享源代码。记得点赞关注收藏&#xff01; 1.实现效果 2.实现步骤 定义css变量&#xff0c;–checked&#xff0c;表示激活选中色值 :root {--checked: orange; }创建父容器&#xf…...

工业机器人编程调试怎么学

很多人觉得工业机器人很难学学&#xff0c;实际上机器人涉及的知识远比PLC要少。现简单说明一下初学者学习工业机器人编程调试的流程&#xff0c;以AUBO机器人为例&#xff1a; 首先我们需要知道工业机器人的调试学起来不难&#xff0c;远比编程更简单&#xff0c;示教器上的编…...

网站建设经营范围/公众号排名优化软件

PropertyDescriptor类&#xff1a; PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法&#xff1a;   1. getReadMethod()&#xff0c;获得用于读取属性值的方法   2. getWriteMethod()&#xff0c;获得用于写入属性值的方法 注&#xff1a;…...

温州优化网站/刘雯每日资讯

IEC62087音视频类设备功耗测量方法&#xff1b;南非偏差&#xff1a;SANS 941 2009年7月23日&#xff0c;欧委会在其官方公报&#xff08;OJ&#xff09;上公布了ErP的电视机实施条例(EC) No 642/2009&#xff0c;并于2009年8月12日开始生效。该条例主要 规定了电视机的生态设计…...

设计网络网站/百度推广手机app下载

https://loj.ac/problem/515 题意&#xff1a;给出n个区间&#xff0c;然后从这些区间中取一个数&#xff0c;然后求将取出的数的平方相加得到的数的种类数。 思路&#xff1a;因为n <100,所以得出的数最大为1e6,.所以将得到的数放在bitset中。复杂度O(n ^ 2 *1e6/32)。按…...

南宁商城网站推广公司/网站排名优化公司

一&#xff1a;Linux防火墙基础&#xff1a; Linux防火墙体系主要工作在网络层&#xff0c;针对TCP/IP数据包实施过滤和限制&#xff0c;属于典型的包过滤防火墙&#xff08;也称网络层防火墙&#xff09;&#xff1b; Linux防火墙体系基于内核编码实现&#xff0c;具有非常稳定…...

wordpress 多菜单/个人网站制作教程

SQL2008 R2 如何使用SQL数据库的.bak备份文件恢复还原数据库&#xff0c;具体步骤如下&#xff1a;1、【开始】菜单 - 选择【Microsoft SQL Server 2008 R2】 - 【SQL Server Management Studio】&#xff0c;如下图&#xff1a;2、选择或输入【服务器名称】- 选择身份验证方式…...

织梦cms官方网站/小说推文推广平台

描述 编写一个程序&#xff0c;将输入字符串中的字符按如下规则排序。 规则 1 &#xff1a;英文字母从 A 到 Z 排列&#xff0c;不区分大小写。 如&#xff0c;输入&#xff1a; Type 输出&#xff1a; epTy 规则 2 &#xff1a;同一个英文字母的大小写同时存在时&#xff0…...