<网络>TCP代码协议编写
温馨提示:这篇文章已超过401天没有更新,请注意相关的内容是否还可用!
首先,我们需要了解什么是TCP。TCP是一种传输控制协议。首先,我们要实现客户端和服务器之间的网络通信。答案是每台主机的网络IP。当我们的电脑访问互联网时,它会自动为我们的电脑分配一个局域网中的IP。网络通信底层是通过硬件网卡进行的。所以这里为了方便起见,我们先封装一个Sock类,以便更好的编写服务端。TCP中的这个fd也是一个sock套接字,称为监听套接字
目录
1、网络通信的本质是什么?
本篇文章我们主要实现如何使用TCP进行完整的网络通信
首先,我们需要了解什么是TCP。 TCP是一种传输控制协议。 现在让我们记住这一点。
现在我们先开始吧。 首先,我们要实现客户端和服务器之间的网络通信。 两个不同主机之间的通信是什么? 答案是每台主机的网络IP。 当我们的电脑访问互联网时,它会自动为我们的电脑分配一个局域网中的IP。
好了,现在我们的两台主机已经找到了对方,接下来我们就看看我们要做什么,两台主机之间的通信是否正确? 说清楚一点,我们需要使用一台主机上的APP向另一台主机上的APP发送消息,而APP在电脑上,启动的时候叫什么? 我们为那些正在运行的应用程序使用一个更专业的名称,称为进程! ! ! ok,就是我们需要大家明白我们到底想要达到什么目的! -- 两台主机中两个进程之间的通信! ! ! 好了,这段话明白到这里了。 流程类似下图:
接下来我们要介绍一个新的概念:端口号,大家首先都承认有这个东西,之前我们都知道,现在两台主机通过IP可以找到对方,现在的问题是:一台主机不能只永远运行一个进程,就像我们同时启动QQ、微信、腾讯视频、浏览器等,假设我们要在两台主机之间进行QQ通信,如何才能在这么多进程中成为唯一的一个并坚定地选择它呢? ? ,这个问题很关键。 那么我们的设计师是如何解决这个问题的呢? 接下来就是重点了:我们可以给每个进程绑定一个端口号(如何绑定代码会体现),并且规定一个端口号只能绑定一个进程,注意:这里一个进程可以绑定这个时候我们在网络上通信的时候,只需要带上对方的主机IP和进程绑定的端口号,就可以准确的进行网络通信了。 是不是很简单啊哈哈哈
二、编写代码--------
那么我们需要在代码中做什么呢?
1.我们需要获得一个IP,对吧?
2、我们需要自定义一个端口号,并绑定这个IP,形成唯一的对应关系。 这里的关系是生成一个socket套接字,即:IP+port(端口号)。
这样我们就可以实现通信的一般逻辑了。 网络通信底层是通过硬件网卡进行的。 这些硬件的操作需要很高的权限,所以显然,网络相关的接口都是在系统级别的! ! !
所以这里为了方便起见,我们先封装一个Sock类,以便更好的编写服务端。
Sock有哪些功能? 本课是关于TCP协议的,我们需要了解TCP通信的细节:
TCP Sock服务编写流程---
1、创建sock套接字:使用socket函数获取网络的fd文件描述符。 TCP中的这个fd也是一个sock套接字,称为监听套接字
2、绑定IP+端口号:通过bind函数将此fd描述符与IP+端口号绑定,实现网络通信的必要条件
3、设置监听状态:通过listen函数将这个fd,也就是sock套接字设置为监听状态,这个sock也称为监听套接字
4、接受连接:再次使用accept函数,通过传入监听的socket和一个struct sockaddr_in类型的输入输出参数来接受客户端的连接请求,如果连接成功则返回一个socket,两个进程网络通信是通过这个新的套接字完成的!
解释一下第3点和第4点,因为TCP是面向连接的,所以当我们正式通信的时候,我们需要先建立连接,那么我们的服务器是不是需要处于等待我们连接的状态呢? 第二个状态是监听状态。 我举个例子方便大家理解。 就像我们去一些餐馆吃饭一样,白天去的时候总是可以吃的。 为什么? 因为那些餐馆的老板总是在等待客人的到来,以便能够及时为客人服务,所以监听状态就是这样,而用来设置监听状态的套接字称为监听套接字。
好了,现在大体流程已经清楚了,那么另一个问题是,我们如何理解监听的socket和新返回的socket呢? !
我们讲一个小故事方便大家理解:假设,你现在去一个地方旅行,你去一个地方吃饭。 这个地方有很多餐厅,每个餐厅都比较内向,雇佣一个人。 这个人什么都不做,只做一件事,那就是去路边招揽顾客,路边就会有人招揽顾客,并把顾客带到店里。 假设顾客是你,这个律师叫李四,李四拉你去店里。 你答应了,于是李四就带你去了餐厅,然后大声喊道,“一个物流服务员出来了,有客人,他跟你打招呼”,于是出来一个服务员张三,于是张三就来给你服务。 当张三为你服务时,李四就跑到路边招揽顾客,如此循环往复。
上面的例子:
监听socket---拉拢李四,工作就是获取新链接
新的socket返回了---是服务员张三
到这里相信大家应该明白这两个套接字的作用和意义了。
通过上面的过程,就实现了两个连接,我们就可以开始通信了!
可以直接使用read和write来读写socket。
3.编写一个Sock类
让我们开始战斗吧! ! !
首先写一个Sock类,其中包含
1.创建sock套接字
2. 绑定IP+端口号
3. 设置监控状态
4. 接受连接
以上四种基本方法
//写一个Scok类
class Sock
{
public:
Sock(){} // 无用
int Socket(){} //1.创建套接字
void Bind(int sock, std::string ip, uint16_t port){} //2.实现套接字和IP、端口号的绑定
void Listen(int listensock){} //3.设置监听套接字
int Accept(){int listensock, std::string& ip, uint16_t& port} //4.建立连接
~Sock(){} // 无用
};
Socket() 实现
基于以上知识,我们需要调用socket系统函数来获取socket,如下
AF_INET:网络系列,使用IPV4
SOCK_STREAM:使用流套接字,TCP是面向字节流的
0:默认即可
int Socket()
{
int _listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock < 0)
{
logMessage(FATAL, "creat listensocke [%d]-[%s]", errno, strerror(errno));
exit(-1);
}
logMessage(NORMAL, "creat listensocke success is : %d",_listensock);
return _listensock;
}
void Bind(int sock, uint16_t port, std::string ip) 的实现
bind系统函数需要使用sockaddr_in结构体,我们可以直接定义初始化参数
// 2.绑定IP+Port
void Bind(int sock, uint16_t port, std::string ip)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)
{
logMessage(FATAL, "bind listensocke [%d]-[%s]", errno, strerror(errno));
exit(1);
}
logMessage(NORMAL, "bind listensocke success!");
}
void Listen(int Listensock) 的实现
这个gblock,以我目前的写作水平还无法详细回答。 到目前为止我所知道的是默认整数 20 就足够了。
// 3.设置监听状态
void Listen(int listensock)
{
if (listen(listensock, gblock) < 0)
{
logMessage(FATAL, "set listensocke to listen status fail [%d]-[%s]", errno, strerror(errno));
exit(1);
}
logMessage(NORMAL, "set listent status success");
}
int Accept(int Listensock, std::string& ip, uint16_t& port) 的实现
具体是,如果连接失败,就进行下一次连接,就像李四没接到顾客,就去找下一个顾客一样,不能直接关店。
// 4.连接
int Accept(int listensock, std::string& ip, uint16_t& port)
{
struct sockaddr_in addr;
socklen_t len = sizeof addr;
while (true)
{
int serverock = accept(listensock, (struct sockaddr*)&addr, &len);
if (serverock < 0)
{
logMessage(ERROR, "get accept fail [%d]-[%s]", errno, strerror(errno));
}
else
{
logMessage(NORMAL, "accpet success [%d]-[%s]", errno, strerror(errno));
ip = inet_ntoa(addr.sin_addr);
port = ntohs(addr.sin_port);
return serverock;
}
}
}
四、Tcp_Server类的编写
我们刚刚封装了Sock,现在写这个类就容易多了。
我们直接大招,看看下面的代码是不是很简单
class Tcp_server
{
public:
Tcp_server(std::string ip = "", uint16_t port = 0)
:_ip(ip), _port(port)
{
Sock c_server;
// 1.创建监听套接字
_listensock = c_server.Socket();
// 2.绑定IP+Port
c_server.Bind(_listensock, _port, _ip);
// 3.设置监听状态
c_server.Listen(_listensock);
}
void Start()
{
Sock c_server;
// 4.连接
while (true)
{
std::string ip;
uint16_t port;
int serversock = c_server.Accept(_listensock, ip, port);
// 开始服务
server(serversock, ip, port); //服务函数等下再编写
close(serversock);
}
}
~Tcp_server()
{
close(_listensock);
}
private:
std::string _ip;
uint16_t _port;
int _listensock;
};
附上服务器功能:这里实现的是一个很简单的逻辑,客户端发送给服务器,服务器返回。
你注意到了吗? 我们在系统文件级别使用 write 和 read 来操作套接字。 这里我们可以看到socket是文件描述。 网络和Linux中的一切都是文件,真的吗? ! ! ! 它呼应了它!
void server(int sock, std::string ip, uint16_t port)
{
char serverBuffer[1024];
while (true)
{
ssize_t s = read(sock, serverBuffer, (sizeof serverBuffer) - 1);
if (s < 0)
{
logMessage(ERROR, "read message fail [%d]-[%s]", errno, strerror(errno));
break;
}
else if (s == 0)
{
logMessage(FATAL, "读端关闭");
break;
}
else if (s > 0)
{
serverBuffer[s] = '\0';
std::cout << "Client --- IP:" << ip << "Port:" << " " << port << std::endl;
std::cout << serverBuffer << std::endl;
}
// 发送回去
write(sock, serverBuffer, sizeof serverBuffer - 1);
}
}
5. 实现来回发送消息的服务器和客户端
最后我们看看并实现一个简单的客户端和服务器
第一的
#include
#include
#include "Tcp_server.hpp"
void Usage(std::string s)
{
std::cout << "Usage "<< s << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(-1 );
}
std::string ip = "0.0.0.0";
uint16_t port = atoi(argv[1]);
Tcp_server server(ip, port);
server.Start();
return 0;
}
的
注意,客户端不需要自己绑定IP和端口号,它会自动分配,因为如果我们自己绑定的话,可能会出现端口号冲突,从而导致另一个客户端无法启动的情况。
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
void Usage(std::string s)
{
std::cout << "Usage "<< s << "ServerIp ServerPort" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(-1);
}
// 编写客户端逻辑
// 1.想要连接的IP
// 2.想要连接的端口
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int sock = socket(AF_INET, SOCK_STREAM, 0);
// 客户端会自动分配端口号
if (connect(sock, (struct sockaddr*)&addr, sizeof addr) < 0)
{
logMessage(FATAL, "client connect fail [%d]-[%s]", errno, strerror(errno));
exit(-1);
}
logMessage(NORMAL, "connect success [%d]-[%s]", errno, strerror(errno));
while (true)
{
std::string line;
std::cout << "输入发送消息:";
std::getline(std::cin, line);
// write(sock, line.c_str(), sizeof line.c_str());
send(sock, line.c_str(), line.size(), 0);
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = '\0';
std::cout << "recv server message:" << buffer << std::endl;
}
}
return 0;
}
编译完成后,运行它们可以看到:
这里使用Centos7.6版本的操作系统
首先运行服务器,注意这里我们没有指定服务器是哪个IP地址,因为我们在代码中使用的默认IP“0.0.0.0”表示服务器的任何IP
87ae6c829143fb9f31339a3ac4afab52
按回车运行后,我们再次启动客户端
171be4527f77ebe2293e63bd24bff0ef
运行后是这样的
04658f8b8902351a164de78231f29595
让我们来看看
dd072994ffe1c4f72118cb8222e8d969
我们发消息试试
c842994116942831e97dd637424c9ec7
可以看到我们成功发送了消息,服务器也返回了消息,到这里我们就实现了一个简单的TCP协议网络通信服务器;
六、附加日志功能
代码中的logMessage是我自己写的一个用于打印日志信息的函数,分享给大家
#pragma once
#include
#include
#include
#include
#include
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define DEBUG_SHOW
const char* gLevel[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
void logMessage(int level, char* format, ... )
{
#ifndef DEBUG_SHOW
if (level == DEBUG) return;
#endif
// 标准
char stdBuffer[1024];
const time_t curt = time(nullptr);
struct tm* _curt = localtime(&curt);
snprintf(stdBuffer, sizeof stdBuffer, "[%s]-[%d年%d月%d日%d时%d分%d秒]", gLevel[level],
_curt->tm_year + 1900, _curt->tm_mon + 1, _curt->tm_mday, ((_curt->tm_hour + 12) % 24) + 12, _curt->tm_min, _curt->tm_sec);
// 自定义
char logBuffer[1024];
va_list args;
va_start(args, format);
vsprintf(logBuffer, format, args);
va_end(args);
FILE* fd = fopen("log_tcp_server", "w");
fprintf(fd, "%s-%s\n", stdBuffer, logBuffer);
fclose(fd);
}