赞
踩
一个UDP程序的编写可以分为3步:
创建一个网络套接字:
它相当于文件操作时的文件描述符,是一个程序进行网络通讯的门户, 所有的网络操作都要基于它
绑定IP和端口:
需要为网络套接字填充IP和端口信息
但是有些时候无需手动填充,让系统自动自动分配即可
发送和接收消息
申请一个套接字
套接字:相当于一个文件描述符,其中存放着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
返回值:返回一个文件描述符
#include <sys/socket.h>
#include <sys/types.h>
int main()
{
int sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd_ < 0)
{
exit(1);
}
}
绑定网络信息
将网络信息写入网络套接字对应的内核区域
#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创建的套接字需要兼容本地、网络等多个域,多个协议,而这些协议需要绑定的信息也不尽相同,对应描述信息的的结构体就不同,如:
、
我们可以用一种多态的理念直接给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. */ };
- 1
- 2
- 3
- 4
- 5
- 6
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地址 };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
addrlen
addr结构体变量的大小
#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
从网络套接字中接收消息
#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表示读取出错
#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; }
这个程序的功能就是从套接字读取一串字符并打印到屏幕
发送一条消息
#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
结构体的大小
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; }
由于不同操作系统,不同编译器有不同的字节序,为了网络通信的方便,网络字节序统一规定为大端,
所有要进行网络传输的数据都要先转为网路字节序,
从网络种接收到的数据也要先转为本地字节序
#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位数转为本地字节序
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
函数名解析:
< h: host --> 本地
n: network --> 网络
l: long --> 32位数
s:short --> 16位数 >
服务器的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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
一个聊天室需要有服务器端和客户端
服务器端负责接收消息,并将收得的消息发送给所有的已登陆用户
客户端负责发送消息,同时接收服务器同步过来的消息
用户登陆方式为:在客户端向服务器发送一条消息
如果用户长时间没有在聊天室发言,将会被提出群聊
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; }
.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编程的前两步也依然是
与udp的区别在于:
创建socket的时候type选择SOCK_STREAM
,的流式套接字
如下为tcp独有的部分:
设置为监听状态(listen)
此步骤不一定要进行,只有被连接一方(服务器)需要设为监听
获取连接(accept)/发起连接(connect)
一般由客户端发起连接(客户端知道服务器的IP和port),服务器获取连接
进行发消息(write)和读消息(read)
一般程序分为服务器端和客户端,编写步骤如下:
服务器端:
客户端:
将socket套接字设为监听状态
因为tcp是面向面向连接的,所以要把当前套接字设为可连接的状态
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
如下是从创建套接字到设置监听状态的三个步骤:
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); } }
让处于监听状态的套接字获取连接,此时如果对端发起connect即可完成连接。
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
两个套接字的区别:
如下代码续接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";
}
向服务器发起连接
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
一般由客户端发起连接,创建套接字之后无需手动绑定,无需设置监听状态,直接向远端发起connect,即可和远端服务器建立连接,在此过程,系统会自动为这个套接字绑定IP和端口,同时自己的网络信息也会被发送到远端。
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); } }
接收网络消息与文件读取用的是同一个函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
如下是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; } }
向网络发送消息和写文件使用同一个API
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
续接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套接字的使用
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; }
上面的字符转换服务器我们分别尝试了单执行流、多进程、多线程的版本
单执行流同一时间只能对一个客户端进行服务,只有该客户端退出才能对下一个客户端进行服务
多线程和多进程的版本使用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服务器就成为了守护进程
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。