> 文章列表 > UDPTCP网络编程

UDPTCP网络编程

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结构体)

    UDPTCP网络编程

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

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

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

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

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

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

    UDPTCP网络编程

    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)

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

服务器端:

客户端:

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

UDPTCP网络编程

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