Linux之网络套接字
- 一.IP地址和端口号
- 二.TCP和UDP协议
- 2.1网络字节序
- 三.socket编程的常见API
- 四.模拟实现UDP服务器和客户端
- 五.模拟实现TCP服务器和客户端
一.IP地址和端口号
在了解了网络相关的基础知识之后我们知道了数据在计算机中传输的流程并且发现IP地址在其中占据了确定目标主机的功能,但是在计算机中我们网络通信的行为其实本质都是进程间进行通信。那么在一个主机中有那么多个进程我们要如何准确定位到某个进程呢?
那就要利用端口号了,所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。
所以我们使用IP地址+端口号的方法就可以找到互联网中唯一的一个进程,其中IP地址用来标记互联网中唯一的一台主机,端口号用来标记主机中唯一的一个进程。但是对于端口号有些地方是和IP地址相同的,一个主机既可以只有一个IP地址也可以有多个IP地址所以一个进程既可以有一个端口号也可以有多个端口号。同时多个主机不可以有一个IP地址多个进程也不可以有一个端口号。
在了解到其中的奥秘之后人们为了更好的称呼这两个概念的组合就又创造了一个概念叫套接字(socket),socket的英译其实是插座的意思从它的英译中我们也可以大概理解它的作用。插座是为了连接两端的物品所以将其具体到网络中socket的作用就是充当应用层和传输层之间的一个抽象层,它将TCP/IP传输层中的一些复杂的操作简单化接口化进而提供给应用层进行调用从而实现进程间的网络通信。
如果端口号port是用来确定计算机中的唯一的一个进程的话,那么可能有些人就有疑问了我们在操作系统的学习的时候使用过进程的pid当时说pid的作用也是确定唯一的一个进程。那么这两者之间有什么联系或者是有什么区别吗?
答案是没有什么区别,它两都可以用来确定主机中唯一的一个进程但是pid是在进行进程管理的时候使用的而端口号是在进行网络通信的时候使用的,这么做的原因其实也很简单就是将进程管理和网络通信进行解耦,pid就专职于进程管理port就专职于网络通信两个人谁都不打扰谁。
二.TCP和UDP协议
之前我们一直在谈协议的分层协议的意义等等但是我们一直没有见过真正的协议如今我们就先学习一下传输层中的两个协议:TCP协议和UDP协议
TCP协议
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
我们先大致了解一下这两个协议的基本特质其中具体的内容我们等之后再谈,在这里我要强调一个概念:可不可靠传输是协议的特性,这两个都是一个中性词没有好坏之分。就像函数的可复用性一样,可靠传输不代表就比不可靠传输好。可靠传输说明传输的过程更复杂更安全,不可靠传输说明传输的过程比较简单适合一些对数据可靠性不高的场景。
2.1网络字节序
在我们最开始学习C语言的时候我们在介绍数据的存储方式时提到过不同的机器存储方式不同其中分为大端机和小端机。大端模式也就是将低位数据存储到高地址中高位数据存储到低地址中,小端模式是将低位数据存储到低地址里高位数据存储到高地址里。大端模式和小端模式没有明显的优劣之差,如果非要说哪个优一点大端机器的可读性比小端机器好一点。
对于内存的中的数据存储方式有大小端之分那么对于网络传输的数据流是否也要有大小端之分呢?我们又该选择哪个呢?
虽然内存地址有大小端之分但是我们要知道计算机对于数据的读取和写入是从低地址到高地址的这也就是导致为什么大端模式比小端模式可读性更高的原因。
所以在数据从一个主机发送到另外一个主机读取的过程中发送和读取都是从低地址到高地址的所以网络数据流的数据的存储方法也就是数据的地址是按照大端模式的也就是低地址高字节。
要注意这是一个规定不是你先用哪个方式发送就是哪个方式的,所有的机器在进行TCP/IP通信的时候发送数据的时候都需要将其转换为大端模式。而这个转换为大端模式后的数据存储方式叫做网络字节序。
而为了方便用户将各种整型变量网络字节序化系统提供了几个库函数
三.socket编程的常见API
- 创建socket文件描述符(TCP/UDP, 客户端 + 服务器)
对于这三个参数和返回值大家应该有不少的疑惑:为什么返回值是一个文件描述符?socket类型是什么?SOCK_STREAM和SOCK_DGRAM又分别是什么意思?我一个一个的给大家解释一下。
对于返回值是文件描述符的问题我们需要知道网络传输的原理是什么,简单来说两个主机互相传输数据其实就是在服务器和客户端中分别创建一个文档然后服务器会向自己新创建的文档中写入和读取数据,而写入的数据又会通过网络拷贝到客户端新创建的文档中而客户端亦是如此。所以在创建套接字的时候会返回一个文件描述符。
在进行网络编程的时候socket是有很多类型的有些是由于传输的环境不同有些是由于协议的不同,例如unix_socket:域间socket,在同一台主机中进行通信,网络socket:使用ip+port的方式运用于网络通信,原始socket:编写一些网络工具。
SOCK_STREAM和SOCK_DGRAM分别是流格式套接字和数据报格式套接字,这两个概念在我们介绍TCP和UDP协议的提到过这也就是为什么这两个socket类型是和TCP和UDP协议组合在一起的原因。
-
绑定端口号 (TCP/UDP, 服务器)
对于第二个参数我需要给大家讲解一下,sockaddr是一个结构体代表的是一个通用的地址类型它其中存储了地址族和套接字中的目标地址信息和端口号。
但是这个结构体是有漏洞的它将目标的地址信息和端口号混在了一起。所以还设计了另外两个结构体sockaddr_in。
而sockaddr和sockaddr_in的就像我们在C++中学习的多态的基类和子类一样,我们在socket编程中使用的一般都是sockaddr_in但是由于某些函数的参数是sockaddr所以我们可以将sockaddr_in强转为sockaddr。 -
开始监听socket (TCP, 服务器)
listen函数的作用是将套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接,具体的会在讲解协议的时候再谈。如今我们只需要知道怎么用它即可。 -
接收请求 (TCP, 服务器)
为什么还要创建一个新文件呢不是已经创建了一个文件了吗为什么不用那个文件描述符呢?
这个问题其实在使用listen的时候就已经有答案了,大家可以仔细看看listen函数的作用:套接字文件描述符从主动转为被动文件描述符,然后用于被动监听客户端的连接。socket返回的文件描述符已经是被动模式那么肯定需要我们再创建一个新的文件来对客户端进行服务。就好像我们在路上遇到的拉客的人,在我们被他拉进去吃饭之后服务我们的是另外的服务员并不是那个拉我们进来的人。使用套接字文件描述符就是那个拉客的人而这个新的文件描述符才是服务我们的人。 -
建立连接 (TCP, 客户端)
在大概了解了模拟实现UDP和TCP服务器和客户端时会出现的新函数后我们就可以来分别模拟实现UDP服务器和客户端以及TCP服务器和客户端了。
这两个协议的不同所以模拟实现的过程也不同并且它两服务器和客户端的运行逻辑也不同,到了模拟实现的时候我会具体来说。
四.模拟实现UDP服务器和客户端
我们先来看一下UDP服务器和客户端的运行逻辑
#Makefile
.PHONY:all
all:udp_server udp_client
udp_server:Main.cc
g++ -o $@ $^ -std=c++14
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -f udp_server udp_client
//log.hpp
#pragma once
#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
// 分为打印到屏幕上,打印到一个文件中,打印到不同的等级的文件中
enum
{
Screen = 10,
Onefile,
Classfile
};
int defaultstyle = Screen;
std::string default_filename = "log.";
std::string logdir = "log";
class Log
{
public:
Log(int style = defaultstyle, std::string file = default_filename)
: _style(style), _file(file)
{
// mkdir不止是指令,还是一个函数可以用来调用然后创建目录
mkdir(logdir.c_str(), 0775);
}
// 更改模式
void Enable(int sty)
{
std::cout << "Enable success" << std::endl;
_style = sty;
}
std::string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
}
return "null";
}
// 返回本地时间
std::string TimeChangeLocal()
{
time_t currtime = time(nullptr);
// 我们利用time返回的时间是以时间戳的形式返回的不太好看
// 所以我们可以利用localtime将其转化为一个类
struct tm *local = localtime(&currtime);
char time_buffer[128];
// 利用snprintf以年-月-日 小时-分钟-秒的形式输入到tim_buffer中
// 注意:tm类的年是以1900年开始的的
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d-%d-%d",
local->tm_year + 1900, local->tm_mon, local->tm_mday,
local->tm_hour, local->tm_min, local->tm_sec);
return time_buffer;
}
// 打印到具体的某个文件中
void WriteLogToOneFile(const std::string &logname, const std::string &message)
{
umask(0);
int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
write(fd, message.c_str(), message.size());
close(fd);
}
// 打印到不同等级的文件中
void WriteLogToClassFile(const std::string &leverstr, const std::string &message)
{
std::string logname = logdir;
logname += "/";
logname += _file;
logname += leverstr;
WriteLogToOneFile(logname, message);
}
void Writelog(std::string &str, std::string &message)
{
switch (_style)
{
case Screen:
std::cout << message;
break;
case Onefile:
WriteLogToClassFile("all", message);
break;
case Classfile:
WriteLogToClassFile(str, message);
break;
default:
break;
}
}
// 我们想要日志像printf一样打印所以要使用可变参数
// 但是我们可以利用va_list参数的变量将可变参数存储在变量中
void LogMessage(int level, const char *format, ...)
{
// 左边信息为:[消息等级][时间][进程编号]
char leftmessage[1024];
std::string leverstr = LevelToString(level);
std::string currtime = TimeChangeLocal();
std::string idstr = std::to_string(getpid());
// 右边信息为:想输出的内容
char rightmessage[1024];
va_list args;
va_start(args, format); // 将args指向可变参数部分
vsnprintf(rightmessage, sizeof(rightmessage), format, args); // 将可变参数写入到rightmessage中
va_end(args); // args == nullptr
snprintf(leftmessage, sizeof(leftmessage), "[%s][%s][%s]", leverstr.c_str(), currtime.c_str(), idstr.c_str());
std::string loginfo = leftmessage;
loginfo += rightmessage;
Writelog(leverstr, loginfo);
}
~Log()
{
}
private:
int _style;
std::string _file;
};
Log lg;
//nocopy.hpp
#pragma once
class NoCopy
{
public:
NoCopy()
{
}
~NoCopy()
{
}
NoCopy(const NoCopy &) = delete;
const NoCopy &operator=(const NoCopy &) = delete;
};
//Main.cc
#include "udp_server.hpp"
#include <iostream>
#include <memory>
void Usage(std::string process)
{
std::cout << "Usage:\n" << process << "local_ip local_port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
std::unique_ptr<UdpServer> up_udp = std::make_unique<UdpServer>(ip,port);
up_udp->Init();
up_udp->Start();
return 0;
}
//udp_client.hpp
#include <iostream>
#include <memory>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
const static int defaultsize = 1024;
void Usage(std::string process)
{
std::cout << "Usage:\n"
<< process << "server_ip server_port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return -1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1.创建socket
int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (socketfd < 0)
{
std::cout << "client socket error" << errno << " " << strerror(errno) << std::endl;
return -1;
}
std::cout << "socket create success socket:" << socketfd << std::endl;
// 2.绑定网络信息
// client需要绑定网络信息但是不需要显式绑定,在client发送第一条消息时系统会随机给他绑定一个端口号
// 为什么client需要绑定网络信息是因为client会有很多如果我们显式绑定很容易重复
// 收集server的信息等到发送时使用
struct sockaddr_in servermessage;
memset(&servermessage, 0, sizeof(servermessage));
servermessage.sin_family = AF_INET;
servermessage.sin_port = htons(server_port);
servermessage.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 3.发送消息
// 服务器是个死循环,客户端同理
while (1)
{
std::string inbuffer;
std::cout << "Please Enter:";
std::getline(std::cin, inbuffer);
// 发送消息
ssize_t n = sendto(socketfd, inbuffer.c_str(), inbuffer.size(),0,(struct sockaddr *)&servermessage, sizeof(servermessage));
if (n > 0)
{
// 接收消息
char outbuffer[defaultsize];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(socketfd, &outbuffer, sizeof(outbuffer) - 1,0,(struct sockaddr*)&temp, &len);
if (m > 0)
{
outbuffer[m] = 0;
std::cout << "server echo:" << outbuffer << std::endl;
}
else
{
break;
}
}
else
{
break;
}
}
close(socketfd);
return 0;
}
//udp_server.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "nocopy.hpp"
const static uint16_t defaultport = 8888;
const static int defaultsocketfd = -1;
const static int defaultsize = 1024;
class UdpServer : public NoCopy
{
public:
UdpServer(std::string ip, uint16_t port = defaultport, int socketfd = defaultsocketfd)
: _ip(ip), _port(port), _socketfd(socketfd)
{
}
~UdpServer()
{
}
void Init()
{
// 1.创建socket,创建文件信息
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));
exit(-1);
}
lg.LogMessage(Info, "socket create success, socketfd:%d\n", _socketfd);
// 2.绑定网络信息
// 保存本地的信息
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
// 在保存本地信息时要将ip和端口号port转换为网络序列
// ip同时要改为四字节的
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
// 本地信息的结构体填完了后要将其加载到内核中
int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{
lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));
exit(-1);
}
}
void Start()
{
char buffer[defaultsize];
// 服务器是一个死循环
while (1)
{
// 保存client客户端的信息
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 接收消息
ssize_t n = recvfrom(_socketfd, &buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say :" << buffer << std::endl;
// 发送消息
sendto(_socketfd, &buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, len);
}
}
}
private:
std::string _ip;
uint16_t _port;
int _socketfd;
};
这些代码只能完成最简单的UDP的服务器和客户端仅仅只能发消息,大家还可以利用我们之前学习的程序替换来完成发送并且执行指令的功能以及利用多线程来完成一个聊天室的功能。
这里的代码我就不贴出来了大家需要的话可以私聊我。
五.模拟实现TCP服务器和客户端
在TCP编程中我们就不需要使用sendto和recvform这两个函数了我们可以直接用write和read来完成文件的读写操作。
下面我也是直接贴出代码,这次的代码就是最终版了比较的复杂,所以我就把主要的关于客户端和服务端的代码贴出来了。
#Makefile
.PHONY:all
all:tcp_server tcp_client
tcp_server:Main.cc
g++ -o $@ $^ -std=c++14 -lpthread
tcp_client:tcp_client.cc
g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
rm -f tcp_server tcp_client
//tcp_client.cc
#include <iostream>
#include <memory>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "log.hpp"
#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
#define Retry_Count 5
const static int defaultsize = 1024;
void Usage(std::string process)
{
std::cout << "Usage:\n"
<< process << "server_ip server_port\n"
<< std::endl;
}
bool VisitServer(std::string server_ip, uint16_t server_port, int *count)
{
// 1.创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
ssize_t m = 0;
ssize_t r = 0;
std::string inbuffer;
char service_list[1024];
if (sockfd < 0)
{
std::cout << "client socket error" << errno << " " << strerror(errno) << std::endl;
return -1;
}
// std::cout << "socket create success socket:" << sockfd << std::endl;
// 2.绑定网络信息
// client需要绑定网络信息但是不需要显式绑定,在client发送第一条消息时系统会随机给他绑定一个端口号
// 为什么client需要绑定网络信息是因为client会有很多如果我们显式绑定很容易重复
// 收集server的信息等到发送时使用
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); // 从字符串ip到4字符ip 从系统序列变为网络序列
bool result = true;
// 3.连接客户端
int n = connect(sockfd, CONV(&server), sizeof(server));
if (n < 0)
{
result = false;
goto END;
}
*count = 0;
std::cout << "connect success"
<< std::endl;
// 3.发送消息和接收信息
r = read(sockfd, &service_list, sizeof(service_list) - 1);
if (r > 0)
{
service_list[r] = 0;
std::cout << "service list is " << service_list << std::endl;
}
std::cout << "Please select service: ";
getline(std::cin, inbuffer);
write(sockfd, inbuffer.c_str(), inbuffer.size());
std::cout << "Enter: ";
getline(std::cin, inbuffer);
if (inbuffer == "quit")
{
return true;
}
m = write(sockfd, inbuffer.c_str(), inbuffer.size());
if (m > 0)
{
char outbuffer[defaultsize];
r = read(sockfd, &outbuffer, sizeof(outbuffer) - 1);
if (r > 0)
{
outbuffer[r] = 0;
std::cout << outbuffer << std::endl;
}
else if (r == 0)
{
return result;
}
else
{
lg.LogMessage(Fatal, "client read error, %d : %s\n", errno, strerror(errno));
result = false;
goto END;
}
}
else
{
lg.LogMessage(Fatal, "client write error, %d : %s\n", errno, strerror(errno));
result = false;
goto END;
}
END:
close(sockfd);
return result;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return -1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
int cnt = 1;
while (cnt <= Retry_Count)
{
bool result = VisitServer(server_ip, server_port, &cnt);
if (result)
{
break;
}
else
{
sleep(1);
std::cout << "client is retring connect, count: " << cnt << std::endl;
cnt++;
}
}
if (cnt > Retry_Count)
{
std::cout << "server is unonline..." << std::endl;
}
return 0;
}
//Main.cc
#include <iostream>
#include <memory>
#include <algorithm>
#include "tcp_server.hpp"
#include "Translate.hpp"
#include "Daemon.hpp"
void Usage(std::string process)
{
std::cout << "Usage:\n"
<< process << " local_port\n"
<< std::endl;
}
void Interact(int fd, InetAddr addr, std::string inbuffer)
{
char outbuffer[1024];
// 先读再写
while (1)
{
ssize_t n = read(fd, &outbuffer, sizeof(outbuffer) - 1);
if (n > 0)
{
outbuffer[n] = 0;
std::cout << "[" << addr.PrintDebug() << "]# " << outbuffer << std::endl;
write(fd, inbuffer.c_str(), inbuffer.size());
}
else if (n < 0)
{
lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));
break;
}
else if (n == 0)
{
lg.LogMessage(Debug, "client quit...\n");
break;
}
}
}
void Ping(int fd, InetAddr addr)
{
lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Ping", fd);
Interact(fd, addr, "pong");
}
translate ts;
void Translate(int fd, InetAddr addr)
{
lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Translate", fd);
char english[128];
ssize_t m = read(fd, &english, sizeof(english) - 1);
if (m > 0)
{
english[m] = 0;
}
std::string chinese = ts.Execute(english);
write(fd, chinese.c_str(), chinese.size());
lg.LogMessage(Debug, "translate success, %s->%s\n", english, chinese.c_str());
}
void Transform(int fd, InetAddr addr)
{
lg.LogMessage(Debug, "%s select %s success, fd:%d\n", addr.PrintDebug().c_str(), "Transform", fd);
char message[128];
int n = read(fd, message, sizeof(message) - 1);
if (n > 0)
message[n] = 0;
std::string messagebuf = message;
std::transform(messagebuf.begin(), messagebuf.end(), messagebuf.begin(), [](unsigned char c)
{ return std::toupper(c); });
write(fd, messagebuf.c_str(), messagebuf.size());
lg.LogMessage(Debug, "transform success, %s->%s\n", message, messagebuf.c_str());
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return -1;
}
uint16_t port = std::stoi(argv[1]);
// 将进程转换为守护进程化
Daemon(false, false);
lg.Enable(Classfile);
lg.LogMessage(Debug, "Daemon success...\n");
std::unique_ptr<TcpServer> tp_tcp = std::make_unique<TcpServer>(port);
tp_tcp->EnrollFunc("ping", Ping);
tp_tcp->EnrollFunc("translate", Translate);
tp_tcp->EnrollFunc("transform", Transform);
tp_tcp->Init();
tp_tcp->Start();
return 0;
}
//tcp_server.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"
#include "nocopy.hpp"
#include "InetAddr.hpp"
#include "threadpool.hpp"
#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
#define ERROR "error"
const static int default_backlog = 1;
using task_t = std::function<void()>;
using callback_t = std::function<void(int, InetAddr &)>;
class TcpServer;
// class ThreadDate
// {
// public:
// ThreadDate(int fd, TcpServer *server_ptr, struct sockaddr_in addr)
// : _fd(fd), _server_ptr(server_ptr), _addr(addr)
// {
// }
// ~ThreadDate()
// {
// close(_fd);
// }
// int GetFd()
// {
// return _fd;
// }
// TcpServer *GetServer()
// {
// return _server_ptr;
// }
// InetAddr GetAddr()
// {
// return _addr;
// }
// int _fd;
// TcpServer *_server_ptr;
// InetAddr _addr;
// };
class TcpServer : public NoCopy
{
public:
TcpServer(uint16_t port)
: _port(port), _isrunning(false)
{
}
~TcpServer()
{
}
void Init()
{
// 1.创建socket套接字
_listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listenfd < 0)
{
lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));
exit(-1);
}
// 在使用tcp时会出现有多个套接字链接到一个端口上的情况,所以我们需要运行端口复用
int opt = 1;
setsockopt(_listenfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
lg.LogMessage(Info, "socket create success, socketfd:%d\n", _listenfd);
// 2.填充本地网络信息并且bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(_port);
int n = bind(_listenfd, CONV(&local), sizeof(local));
if (n != 0)
{
lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));
exit(-1);
}
lg.LogMessage(Info, "bind success, listenfd:%d\n", _listenfd);
// 3.将socket设置为监听状态
// 因为tcp协议是依靠链接来进行交互的所以我们需要将客户端的套接字
// 从主动设置为被动从而监听用户端的连接行为
int m = listen(_listenfd, default_backlog);
if (m != 0)
{
lg.LogMessage(Fatal, "listen error, %d : %s\n", errno, strerror(errno));
exit(-1);
}
lg.LogMessage(Info, "listen success, listenfd:%d\n", _listenfd);
threadpool<task_t>::Getinstance()->ThreadStart();
_funcs.insert(std::make_pair("defaulthandle", std::bind(&TcpServer::DefaultHandle, this, std::placeholders::_1, std::placeholders::_2)));
}
void Service(int sockfd, InetAddr addr)
{
char outbuffer[1024];
// 先读再写
while (1)
{
ssize_t n = read(sockfd, &outbuffer, sizeof(outbuffer) - 1);
if (n > 0)
{
outbuffer[n] = 0;
std::cout << "[" << addr.PrintDebug() << "]# " << outbuffer << std::endl;
std::string inbuffer = "pong";
write(sockfd, inbuffer.c_str(), inbuffer.size());
}
else if (n < 0)
{
lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));
break;
}
else if (n == 0)
{
lg.LogMessage(Debug, "client quit...\n");
break;
}
}
}
// 因为这个函数是成员函数所以会带有一个this指针从而导致参数不对
// 所以我们需要将其转换为静态成员函数
// static void *HandleFunction(void *args)
// {
// // 线程分离
// pthread_detach(pthread_self());
// ThreadDate *td = static_cast<ThreadDate *>(args);
// td->GetServer()->Service(td->GetFd(),td->GetAddr());
// delete td;
// return nullptr;
// }
void Routinue(int fd, InetAddr addr)
{
_funcs["defaulthandle"](fd, addr);
std::string type = Read(fd);
lg.LogMessage(Debug, "%s select %s\n", addr.PrintDebug().c_str(), type.c_str());
if (type == "ping")
{
_funcs[type](fd, addr);
}
else if (type == "translate")
{
_funcs[type](fd, addr);
}
else if (type == "transform")
{
_funcs[type](fd, addr);
}
else
{
}
close(fd);
}
void EnrollFunc(std::string name, callback_t func)
{
_funcs[name] = func;
}
std::string Read(int fd)
{
char type[128];
ssize_t n = read(fd, &type, sizeof(type) - 1);
if (n > 0)
{
type[n] = 0;
lg.LogMessage(Debug,"server read success, message:%s\n",type);
}
else if (n < 0)
{
lg.LogMessage(Fatal, "server read error, %d : %s\n", errno, strerror(errno));
return ERROR;
}
else if (n == 0)
{
lg.LogMessage(Debug, "client quit...\n");
return ERROR;
}
return type;
}
void DefaultHandle(int fd, InetAddr addr)
{
std::string lists = "|";
for (auto &ch : _funcs)
{
lists += ch.first;
lists += "|";
}
write(fd, lists.c_str(), lists.size());
}
void Start()
{
// 服务器是一个死循环
_isrunning = true;
signal(SIGCHLD, SIG_IGN);
while (_isrunning)
{
sleep(1);
// 4.获取连接
// 当客户端进入监听状态后我们还需要让客户端使用accept函数来获取连接也就是获取一个全新的文件描述符
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listenfd, CONV(&peer), &len);
if (sockfd < 0)
{
lg.LogMessage(Fatal, "accept error, %d : %s\n", errno, strerror(errno));
continue;
}
lg.LogMessage(Debug, "accept success, get a new sockfd:%d\n", sockfd);
// 5.提供服务
// 版本1
// 问题:只能有一个客户端连接服务器
// Service(sockfd);
// close(sockfd);
// 版本2
// 利用多进程会继承文件描述符表来解决版本1的问题
// pid_t id = fork();
// if (id == 0)
// {
// // 子进程
// // 子进程需要关闭不需要的文件描述符即_listenfd
// close(_listenfd);
// pid_t iid = fork();
// if (iid > 0)
// {
// //子进程
// exit(0);
// }
// else
// {
// //孙子进程
// //在子进程退出后孙子进程就变成了孤儿进程
// //然后就被系统领养
// Service(sockfd);
// close(sockfd);
// exit(0);
// }
// }
// else if (id > 0)
// {
// // 父进程
// // 子进程需要关闭不需要的文件描述符即sockfd
// close(sockfd);
// pid_t rid = waitpid(id, nullptr, 0);
// if (rid == id)
// {
// lg.LogMessage(Debug, "wait success...\n");
// }
// }
// else
// {
// lg.LogMessage(Fatal, "fork error, %d : %s\n", errno, strerror(errno));
// close(sockfd);
// continue;
// }
// 版本3
// 利用多进程中子进程退出时会发出信号后修改信号的处理方法
// 从而让父进程不用等待子进程,系统会自动回收子进程的资源
// pid_t id = fork();
// if (id == 0)
// {
// // 子进程
// // 子进程需要关闭不需要的文件描述符即_listenfd
// close(_listenfd);
// Service(sockfd);
// close(sockfd);
// exit(0);
// }
// else if (id > 0)
// {
// // 父进程
// // 子进程需要关闭不需要的文件描述符即sockfd
// close(sockfd);
// }
// else
// {
// lg.LogMessage(Fatal, "fork error, %d : %s\n", errno, strerror(errno));
// close(sockfd);
// continue;
// }
// 版本4
// 利用多线程会共享一个文件描述符表的特性来让新线程去执行服务
// 并且将新线程进行分离从而达到自动回收资源的目的
// ThreadDate *td = new ThreadDate(sockfd, this, peer);
// pthread_t tid;
// pthread_create(&tid, nullptr, HandleFunction, td);
// 版本5
// 利用线程池提前创建线程并向其分配任务
// 问题:不能给客户端提供长服务
task_t ts = std::bind(&TcpServer::Routinue, this, sockfd, InetAddr(peer));
threadpool<task_t>::Getinstance()->Push(ts);
}
}
private:
uint16_t _port;
// int _fd;
int _listenfd;
bool _isrunning;
std::unordered_map<std::string, callback_t> _funcs;
};
如果大家需要全部的代码可以去我的gitee中自行拷贝或者私信我。