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结构体)
、
我们可以用一种多态的理念直接给bind函数传入两种类型结构体变量的首地址,当函数内要获取网络信息的时候,先读前16位知道当前要绑定信息的域和协议
进而再对后面的位进行特定化读取
这个addr参数完全可以用一个void*来接收两种不同的结构体指针,但是由于一些历史原因,当时还没有void*的语法
所以,函数编写者新定义了一个结构体
struct sockaddr
用法也很简单,只需要把
struct sockaddr_in*
或struct sockaddr_un*
强转为struct sockaddr*
传入即可,bind函数内部会自动通过通过前16位判断要选择哪种数据类型的绑定
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); //拿到对方的IP
uint16_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 3
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 = NULL
FILE* 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_in
struct 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_ANY
local.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)
{
// demo2
char 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); // 拿到对方的port
checkOnlineUser(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);
//发送消息给server
sendto(sockfd, bufferOut.c_str(), bufferOut.size(), 0,
(struct sockaddr *)&server, sizeof(server));
}
return 0;
}
makefile
.PHONY:all
all:udpClient udpServer
udpClient:udpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udpClient udpServer
tcp编程接口
tcp编程的前两步也依然是
- 创建socket套接字
- 绑定网络信息
与udp的区别在于:
创建socket的时候type选择SOCK_STREAM
,的流式套接字
如下为tcp独有的部分:
-
设置为监听状态(listen)
此步骤不一定要进行,只有被连接一方(服务器)需要设为监听
-
获取连接(accept)/发起连接(connect)
一般由客户端发起连接(客户端知道服务器的IP和port),服务器获取连接
-
进行发消息(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); // port
inet_aton(server_ip.c_str(), &server.sin_addr); // ip
// 2.2发起请求,connect会自动选择合适的IP和port进行bind
if (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:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::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版本,多进程,创造孤儿进程,无需忽略SIGCHILD
pid_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()); // 设置线程分离,无需主线程join
ThreadData *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); // port
inet_aton(server_ip.c_str(), &server.sin_addr); // ip
// 2.2发起请求,connect会自动选择合适的IP和port进行bind
if (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:
// sock
int listenSock_;
// port
uint16_t port_;
// ip
std::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 3
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 = NULL
FILE* 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. 忽略SIGPIPE
signal(SIGPIPE, SIG_IGN);
// 2. 更改进程的工作目录
// chdir();
// 3. 让自己不要成为进程组组长
if (fork() > 0)
exit(0);
// 4.设置自己时一个独立的会话
setsid();
// 5. 重定向0,1,2
int fd = 0;
if ((fd = open("/dev/null", O_RDWR) != -1))
{
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
// 关闭掉不需要的fd
if (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 = NULL
int 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缓冲区的数据刷新到OS
fsync(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 tcpServerd
tcpClient:tcpClient.cc
g++ -o $@ $^ -std=c++11 -lpthread
tcpServerd:tcpServer.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f tcpClient tcpServerd

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