一、简单理解ip地址和端口号
1、ip地址
- ip地址 = 网络号 + 主机号
网络号: 保证相互连接的两个网段具有不同的标识。
主机号: 同一网段内, 主机之间具有相同的网络号, 但是必须有不同的主机号。- ip地址在网络中标识主机的唯一性。
- 分为ipv4(头部20字节,32位地址)和ipv6(头部40字节,128位地址)。
- 使用点分十进制来表示,如ipv4:192.168.1.1。
2、端口号
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
- 一个端口号只能被一个进程绑定。
3、总结
ip地址+端口号能识别网络中的某主机的某主机。
二、UDP与TCP特点
1、UDP
- 无连接: 知道对端的 IP 和端口号就直接进行传输, 不需要建立连接;
- 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP 协议层也不会给应用层返回任何错误信息;
- 面向数据报: 不能够灵活的控制读写数据的次数和数量(即应用层交给UDP多长报文,UDP原样发送,既不会拆分也不会合并,每个数据报独立处理,互不影响);
2、TCP
- 面向连接:通过三次握手建立可靠连接。
- 可靠性:通过序列号、确认应答、重传机制和校验和确保数据传输的可靠性。
- 流量控制:通过滑动窗口协议避免接收方缓冲区溢出。
- 拥塞控制:动态调整发送速率,避免网络拥塞。
- 面向字节流:不保留消息边界,适合各种类型的应用。
- 全双工通信:支持同时发送和接收数据。
- 高效的错误恢复:自动重传丢失或损坏的数据段。
三、Socket编程相关接口
介绍:
- Socket提供了一种在不同主机之间进行通信的机制。通过 Socket,应用程序可以在网络上发送和接收数据,实现跨主机、跨平台的通信。
- Socket 主要有两种类型,分别对应 TCP 和 UDP 协议。
1、头文件
#include <sys/types.h>
#include <sys/socket.h>
2、创建socket
功能
函数创建一个新的socket。
函数原型
int sockfd = socket(int domain, int type, int protocol);
参数
- domain(地址族) 指定套接字使用的地址族,常见的值有:
AF_INET:用于 IPv4 地址。
AF_INET6:用于 IPv6 地址。
AF_UNIX:用于本地进程间通信(IPC),不涉及网络。- type(套接字类型) 指定套接字的类型,常见的值有:
SOCK_STREAM:面向连接的可靠传输协议(TCP)。
SOCK_DGRAM:无连接的不可靠传输协议(UDP)。
SOCK_RAW:原始套接字,用于直接访问网络层协议(如 IP)。
SOCK_SEQPACKET:有序的、可靠的数据报协议(较少使用)。- protocol(协议) 指定使用的协议。通常设置为 0,表示使用默认协议。
如果需要指定特定协议,可以使用以下值:
TCP:协议号为 IPPROTO_TCP。
UDP:协议号为 IPPROTO_UDP。
其他协议:如 IPPROTO_ICMP(ICMP)等。
返回值
- 成功返回一个socket描述符(类似于文件描述符)。
- 失败返回 -1。
3、绑定
功能
将Socket绑定到本地地址和端口。
函数原型
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
- sockfd :sockfd描述符。
- addr:这是一个指向 struct sockaddr 的指针,用于指定绑定的本地地址和端口。根据地址族的不同,实际使用的结构体可能是:
IPv4:struct sockaddr_in
IPv6:struct sockaddr_in6
Unix 域套接字:struct sockaddr_un在设置好后传参需要进行强制类型转换为struct sockaddr指针。- addlen:这是地址结构的大小,通常通过 sizeof() 函数计算得到。
对于客户端来说可以不进行bind,如果不进行bind的话操作系统会随机绑定一个没被使用的端口号。
struct sockaddr_in:
#include <netinet/in.h> // 包含 IPv4 相关定义
struct sockaddr_in {
sa_family_t sin_family; // 地址族
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IPv4 地址
};
- sin_family :设置为AF_INET。
- sin_port : 16为端口号。
- sin_addr:填一个ipv4地址,INADDR_ANY:一个特殊的值,表示绑定到所有可用的网络接口。其值为 0xFFFFFFFF,通常用于服务器端,表示监听所有网络接口上的连接。
struct in_addr:
struct in_addr {
in_addr_t s_addr; // IPv4 地址 真实类型: uint32_t 或 unsigned long
};
网络字节序
在内存储存中存在大小端之分,当然在网络数据流中也存在大小端之分。
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节, 不管这台主机是大端机还是小端机, 都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。
- 网络传输统一使用大端字节序(Big-Endian) 作为标准(称为网络字节序)。无论发送端和接收端的本地字节序如何,应用层必须将多字节数据(如整数、端口号)转换为网络字节序后再发送。
如果发送端是小端机器,需在发送数据前调用转换函数(如 htonl() 或 htons()),将本地小端数据转换为大端格式。
接收端(大端机器)需调用逆转换函数(如 ntohl() 或 ntohs()),将网络字节序转换回本地字节序。即使接收端本身是大端机器。
例如
若发送端未转换直接发送小端数据,接收端会按大端解析,导致数值错误。例如:发送端小端发送 0x1234(内存顺序 0x34 0x12)。
若传输的是字节流(如字符串、文本、已序列化的数据),且协议明确约定字节顺序,则无需额外转换。例如传输ASCII字符串 “1234”,字节序不影响解析。
接收端大端会解析为 0x3412(而非正确的 0x1234)。
#include <arpa/inet.h>
//主机转网络
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
//网络转主机
uint16_t ntons(uint16_t hostshort);
uint32_t ntonl(uint32_t hostlong);
//网络ip转为主机ip --将字符串192.168.1.1 -> 网络字节序二进制形
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//af:地址族
//src:网络字符串
//dst:需要转到的主机序
//size:sizeof(dst)
//成功:转换后的dst的地址
//失败:nullptr
根据上面的接口,对struct in_addr进行封装
class InetAddr
{
private:
//端口号网络转主机
void PortNet2Host()
{
_prot = ::ntohs(_addr.sin_port);
}
//ip网络转主机
void IpNet2Host()
{
char ipbuffer[64];
const char *ip = ::inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
(void)ip;
}
public:
InetAddr() = default;
//ip到时默认使用INADDR_ANY
InetAddr(uint16_t prot)
: _prot(prot),
_ip("")
{
_addr.sin_family = AF_INET; //设置地址族
_addr.sin_port = ::htons(_prot); //设置端口号
_addr.sin_addr.s_addr = INADDR_ANY; //设置为INADDR_ANY
}
//拷贝构造
InetAddr(const struct sockaddr_in &addr) : _addr(addr)
{
//将网络转主机
PortNet2Host();
IpNet2Host();
}
//长度
socklen_t Addr_size()
{
return sizeof(_addr);
}
//端口号
uint16_t GetProt()
{
return _prot;
}
//ip
std::string GetIp()
{
return _ip;
}
//返回struct sockaddr *
struct sockaddr * GetAddr()
{
return (struct sockaddr *) &_addr;
}
~InetAddr() = default;
private:
//成员变量保持主机端的字节序
uint16_t _prot;
std::string _ip;
struct sockaddr_in _addr;
};
4、进入监听(TCP)
功能
使Socket进入监听状态,等待客户端连接。
函数原型
int listen(int sockfd, int backlog);
参数
- sockfd(套接字描述符) 这是通过 socket() 函数创建的套接字描述符,用于监听客户端的连接请求。在调用 listen() 之前,套接字必须已经通过 bind() 绑定到一个本地地址和端口。
- backlog(最大连接队列长度) 指定操作系统可以为该套接字排队的最大连接数。当新的客户端连接请求到达时,操作系统会将其放入一个等待队列中,直到服务器通过 accept() 接受连接为止。如果队列已满,新的连接请求将被拒绝。
返回值
成功:返回 0。
失败:返回 -1。
5、建立连接(TCP)
功能
接受客户端的连接请求,返回一个新的Socket用于通信。
函数原型
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数
- sockfd(监听套接字描述符) 这是通过 socket() 创建并已经调用 listen() 进入监听状态的套接字描述符。accept() 会从这个套接字的连接队列中提取一个已建立的连接。
- addr(客户端地址) 指向 struct sockaddr 的指针,用于存储连接的客户端地址信息。如果不需要获取客户端地址,可以设置为 NULL。
- addrlen(地址长度) 指向 socklen_t 的指针,用于指定 addr 的大小。
返回值
成功:返回一个新的套接字描述符,用于与客户端通信。
失败:返回 -1。
阻塞与不阻塞
阻塞
如果套接字处于阻塞模式(默认行为),accept() 会一直等待,直到有客户端发起连接请求为止。这意味着:如果没有客户端连接请求,accept() 会阻塞当前线程或进程,不会返回。一旦有客户端连接请求到达,accept()会从连接队列中提取一个连接,并返回一个新的套接字描述符,用于与该客户端通信。
不阻塞
如果套接字被设置为非阻塞模式,accept() 的行为会有所不同: 如果连接队列中有待处理的连接请求,accept()会立即返回一个新的套接字描述符。 如果连接队列为空(即没有待处理的连接请求),accept() 会立即返回 -1,并设置 errno 为 EAGAIN 或EWOULDBLOCK。
设置状态
#include <unistd.h>
#include <fcntl.h>
int sockfd; // 假设这是已经创建的套接字描述符
// 获取当前套接字标志
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL failed");
return -1;
}
// 清除 O_NONBLOCK 标志(设置为阻塞模式)
if (fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK) == -1) {
perror("fcntl F_SETFL failed");
return -1;
}
//设置为非阻塞
if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL failed");
return -1;
}
6、客户端连接(TCP)
功能
用于建立 TCP 连接的函数。它用于客户端,将客户端套接字与服务器的地址和端口建立连接。
函数原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数
- sockfd(套接字描述符) 这是通过 socket() 创建的套接字描述符,用于建立连接。客户端套接字必须处于未连接状态。
- addr(服务器地址) 指向 struct sockaddr 的指针,用于指定服务器的地址和端口。对于 IPv4,通常使用 struct sockaddr_in;对于 IPv6,使用 struct sockaddr_in6。
- addrlen(地址长度) 指定 addr 的大小(以字节为单位)。
返回值
成功:返回 0。
失败:返回 -1。
对于绑定行为
connect() 函数的主要作用是建立与服务器的连接。它不会自动调用 bind(),但会在内部完成绑定操作。具体来说,connect() 会自动为客户端套接字分配一个临时的本地端口(如果尚未绑定),以便建立连接。这意味着客户端不需要显式调用 bind(),除非有特殊需求。如果客户端套接字已经绑定到本地地址和端口(通过显式调用 bind()),connect() 会使用这个绑定的地址和端口建立连接。
7、发送读取信息
TCP读取数据
函数原型
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数
- sockfd 这是通过 socket() 创建的套接字描述符,用于接收数据。该套接字必须已经通过 connect() 建立了连接(对于客户端)或通过 accept() 接受了连接(对于服务器)。
- buf 指向存储接收到的数据的缓冲区。数据会被复制到这个缓冲区中。
- len 指定缓冲区的大小(以字节为单位)。recv() 最多会接收 len 字节的数据。
- flags 用于控制接收操作的行为。常见的标志包括:
0:默认行为,阻塞直到数据到达。
MSG_PEEK:查看数据但不从接收队列中移除。
MSG_WAITALL:阻塞直到接收到请求的全部数据。
MSG_DONTWAIT:非阻塞模式,立即返回。
返回值
成功:返回读取的字节大小。
失败:返回-1。
对端关闭:返回0。
TCP发送数据
函数原型
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数
- sockfd 这是通过 socket() 创建的套接字描述符,用于发送数据。该套接字必须已经通过 connect() 建立了连接(对于客户端)或通过 accept() 接受了连接(对于服务器)。
- buf 指向要发送的数据的缓冲区。数据将从这个缓冲区复制到网络中。
- len 指定要发送的数据的长度(以字节为单位)。send() 会尝试发送 len 字节的数据。
- flags 用于控制发送操作的行为。常见的标志包括:
0:默认行为,阻塞直到数据发送完成。
MSG_DONTWAIT:非阻塞模式,立即返回。
MSG_MORE:表示还有更多数据要发送(用于 TCP 的 Nagle 算法优化)。
MSG_NOSIGNAL:发送时不会产生 SIGPIPE 信号(即使对端关闭连接)。
返回值
成功:返回发送的实际字节数。
失败:返回-1。
UDP读取数据
函数原型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数
- socket :socket描述符。
- buf:输出型参数,数据会拷贝这个缓冲区中。
- len:buf大小。
- flags:常见的标志包括: 0:默认行为,阻塞直到数据到达。 MSG_PEEK:查看数据但不从接收队列中移除。 MSG_WAITALL:阻塞直到接收到请求的全部数据。
- src_addr:发送方的 struct sockaddr*。
- addrlen:src_addr大小。
返回值:
成功:返回接收到的数据字节数。
失败:返回 -1。
UDP发送数据
函数原型
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数
- socket :socket描述符。
- buf :发送的数据。
- len: 发送数据的大小
- flags(标志) 用于控制发送操作的行为。常见的标志包括: 0:默认行为。 MSG_DONTROUTE:禁止路由,直接发送到本地网络。 MSG_CONFIRM:请求确认报文是否发送成功(仅在某些协议中有效)。
- dest_addr:发送的目标struct sockaddr。
- addrlen:dest_addr的大小。
8、关闭socket
使用int close(int socketfd);
。
四、实现简单UDP服务客户端
1、功能 :实现中文转英文。
单词转换类:进行读取英文-中文的文件,并将中文转换为英文。
#include <iostream>
#include <unordered_map>
#include <string>
#include <fstream>
//默认参数
static const std::string gdefault_dict = "dict.txt"; //文件名
static const std::string gsep = ": "; //分割符
class Dict
{
public:
Dict()
{
std::ifstream file(gdefault_dict); // 打开文件
std::string line;
std::string chinese;
std::string english;
if (!file.is_open()) //判断文件是否打开
{
std::cout << "Unable to open file" << std::endl;
}
while (std::getline(file, line)) //从文件读取数据
{
//字符串分割
auto pos = line.find(gsep);
if(pos != std::string::npos)
{
english = line.substr(0,pos);
chinese = line.substr(pos+gsep.size());
_ce[chinese] = english;
_ce[english] = chinese;
}
}
//关闭文件
file.close();
}
//返回映射
std::string Translate(const std::string s)
{
auto pos = _ce.find(s);
if(pos == _ce.end())
{
return "不存在";
}
return pos->second;
}
~Dict() = default;
private:
std::unordered_map<std::string, std::string> _ce;
};
UDP服务端
功能:接收中文,在通过单词转换类转换为英文,再将英文发送给客户端。
实现:
- 通信:
创建socket —>socket
进行绑定 —>bind
进行读写 —>recvfrom、sendto- 处理客户端信息
通过传入回调函数来处理接收的信息,再返回需要的数据,再发送给客户端。
Udpserver.hpp
//LOG:用于打印日志信息
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <functional>
#include "Log.hpp" //日志
#include "InetAddr.hpp"
using namespace LogMudule; //日志空间作用域
//一些默认参数
static const int gsocketfd = -1;
static const uint16_t dafault_prot = 6654; //自己设置好端口号
static const int gsize = 1024;
using func_t = std::function<std::string(const std::string&)>;
class UdpServer
{
public:
UdpServer(func_t handle, uint16_t prot = dafault_prot)
: _prot(prot)
,_addr(prot)
,_handle(handle)
{
}
// 初始化服务器
void InitServer()
{
// 1.创建socket文件描述符
_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(LogLevel::ERROR) << "socket error...";
}
LOG(LogLevel::DEBUG) << "socket:"<<_socketfd;
// 2进行绑定
int n = ::bind(_socketfd, _addr.GetAddr(), _addr.Addr_size());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind error...";
}
LOG(LogLevel::DEBUG) << "绑定成功";
}
// 开始
void Start()
{
LOG(LogLevel::DEBUG) << "启动服务器";
while (true)
{
// 1.接受数据
char buffer[gsize];
struct sockaddr_in client_addr;
socklen_t client_size = sizeof(client_addr);
InetAddr client_tmp(client_addr);
int n = ::recvfrom(_socketfd, buffer, gsize - 1, 0, client_tmp.GetAddr(), &client_size);
if (n < 0)
{
LOG(LogLevel::ERROR) << "recvfrom error...";
}
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "读取到数据:" << buffer;
//2. 对请求进行处理
std::string ret = _handle(buffer);
LOG(LogLevel::DEBUG) << "处理后的数据:" << ret;
//3. 回复
n = ::sendto(_socketfd, ret.c_str(), ret.size(), 0, client_tmp.GetAddr(), sizeof(client_addr));
if (n < 0)
{
LOG(LogLevel::ERROR) << "sendto error...";
}
}
}
~UdpServer()
{
//关闭socket
int n = ::close(_socketfd);
(void)n;
}
private:
uint16_t _prot; // 端口号
int _socketfd; // 文件描述符
InetAddr _addr;
func_t _handle; //处理方法 将中文转换成英文
};
Udpserver.cc
#include"UdpServer.hpp"
#include"Dict.hpp"
using namespace LogMudule;
int main()
{
Dict d;
UdpServer us([&d](const std::string&s){return d.Translate(s);});
us.InitServer();
us.Start();
return 0;
}
UDP客户端
通信部分和服务端差不多,客户端这里不进行bind,由操作系统绑定。
UdpCilent.hpp
#pragma once
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include "Log.hpp" //日志
using namespace LogMudule; //日志空间作用域
class UdpCilent
{
public:
UdpCilent(std::string ip, uint16_t prot)
: _ip(ip),
_prot(prot)
{
}
// 初始化客户端
void InitClient()
{
// 1.创建socket文件描述符
_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd < 0)
{
LOG(LogLevel::ERROR) << "socket error...";
}
LOG(LogLevel::DEBUG) << "socket:" << _socketfd;
}
// 开始
void Start()
{
LOG(LogLevel::DEBUG) << "启动服务器";
while (true)
{
// 1.发送数据
std::cout << "Please Enter# ";
std::string message;
std::getline(std::cin, message);
struct sockaddr_in server_addr;
::bzero(&server_addr, sizeof(server_addr)); //初始化server_addr空间
server_addr.sin_port = ::htons(_prot);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
int n = ::sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (n < 0)
{
LOG(LogLevel::ERROR) << "sendto error...";
}
LOG(LogLevel::DEBUG) << "发送的数据:" << message;
// 2.接收数据
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
n = ::recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
if(n < 0)
{
LOG(LogLevel::ERROR) << "recvfrom error...";
}
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "收到的数据:" << buffer;
}
}
~UdpCilent()
{
int n = ::close(_socketfd);
(void)n;
}
private:
uint16_t _prot; // 端口号
std::string _ip; // ip地址
int _socketfd; // 文件描述符
};
UdpCilent.cc
#include"UdpClient.hpp"
int main(int argc, char *argv[])
{
//判断参数是否符合
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
}
//对命令行参数进行处理
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
//开始通信
UdpCilent uc(serverip,serverport);
uc.InitClient();
uc.Start();
return 0;
}
五、实现简单的TCP服务客户端
功能:对接收到的信息,原封不动进行回显。
实现:
- 通信
获取socket —>socket
进行绑定 —>bind
设置为监听状态 —>listen
建立连接 --accept
进行读写 —>recv、send- 处理信息
原路返回
TcpServer.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>
#include "Log.hpp" //日志
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
using namespace LogMudule; //日志命名空间作用域
//一些默认参数
static const int gsocketfd = -1;
static const int gbacklog = 8;
static const int gsize = 4096;
class TcpServer
{
private:
//读写函数
void Handle(int socketfd)
{
LOG(LogLevel::DEBUG) << "进入处理";
char buffer[gsize];
while (1)
{
// 读取
LOG(LogLevel::DEBUG) << "进入读取";
int n = read(socketfd, buffer, gsize - 1); //这里使用recv()更好
// 读取成功,进行回显
if (n > 0)
{
buffer[n] = 0;
LOG(LogLevel::DEBUG) << "读取成功:" << buffer;
int m = send(socketfd, buffer, n, 0);
if (m < 0)
{
LOG(LogLevel::ERROR) << "send err...";
break;
}
}
// 客户端退出
else if (n == 0)
{
LOG(LogLevel::DEBUG) << "用户退出";
break;
}
// 出错
else
{
LOG(LogLevel::ERROR) << "read err...";
printf("Error occurred: %d (%s)\n", errno, strerror(errno));
break;
}
}
//关闭文件
int n = ::close(socketfd);
(void)n;
}
public:
TcpServer(uint16_t port)
: _port(port),
_listenfd(gsocketfd),
_addr(port)
{
}
void InitServer()
{
LOG(LogLevel::DEBUG) << "准备进行初始化";
// 1.创建socket
_listenfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_listenfd < 0)
{
LOG(LogLevel::ERROR) << "socket err...";
return;
}
LOG(LogLevel::DEBUG) << "socket : "<< _listenfd <<"端口号:"<<_addr.GetProt();
// 2.绑定
int n = ::bind(_listenfd, _addr.GetAddr(), _addr.Addr_size());
if (n < 0)
{
LOG(LogLevel::ERROR) << "bind err...";
return;
}
// 3.设置为监听
n = ::listen(_listenfd, gbacklog);
if (n < 0)
{
LOG(LogLevel::ERROR) << "listen err...";
return;
}
LOG(LogLevel::DEBUG) << "完成初始化";
}
void Start()
{
LOG(LogLevel::DEBUG) << "启动服务";
while (1)
{
// 4.获取新的套接字
struct sockaddr_in tmp;
::bzero(&tmp, sizeof(tmp)); //初始化tmp
socklen_t len = sizeof(tmp);
int socketfd = ::accept(_listenfd, (struct sockaddr *)&tmp, &len);
if (socketfd < 0)
{
continue;
}
LOG(LogLevel::DEBUG) << "新套接字:" << socketfd;
// 5.读取+处理+回显
//只能处理单个客户端
//Handle(socketfd);
//多进程版本 --能处理多个客户端
// pid_t id = fork();
// if(id == 0)
// {
// //关闭不用的文件描述符
// ::close(_listenfd);
// //让孙子进程处理,子进程退出
// if(fork() > 0)
// {
// exit(0);
// }
// Handle(socketfd);
// exit(0);
// }
// int rid = ::waitpid(id,nullptr,0);
// if(rid < 0)
// {
// LOG(LogLevel::ERROR) << "waitpid err...";
// }
}
}
~TcpServer() = default;
private:
int _listenfd;
uint16_t _port;
InetAddr _addr;
};
客户端
使用状态机的机制来实现能够进行断线重连功能。
实现:
- 定义状态。
- 实现通信部分,和服务端差不多,连接时需要使用 —>connect。
- 在不同情况下将状态进行相应的设置。
- 实现函数来控制不同状态下处理不同函数,当服务端断线后进行重连。
TcpCilent.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>
//设置状态
enum Status //
{
NEW, // 新建状态,就是单纯的连接
CONNECTING, // 正在连接,仅仅方便查询conn状态
CONNECTED, // 连接或者重连成功
DISCONNECTED, // 重连失败
CLOSED // 连接失败,经历重连,无法连接
};
//默认参数
const int gretry_interval = 1; //重连时间
const int gmax_retries = 5; //最大重连次数
class ClientConnection
{
public:
ClientConnection(std::string serverip, uint16_t serverport, int retry_interval = gretry_interval, int max_retries = gmax_retries)
: _serverip(serverip),
_serverport(serverport),
_retry_interval(retry_interval),
_max_retries(max_retries),
_sockfd(-1),
_status(Status::NEW)
{
}
// 连接
void Connect()
{
// 获取fd
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
std::cout << "_sockfd error..." << std::endl;
return;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(_serverport);
server_addr.sin_addr.s_addr = inet_addr(_serverip.c_str());
// client 不需要显示的进行bind, tcp是面向连接的, connect 底层会自动进行bind
int n = ::connect(_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (n < 0)
{
std::cout << "connect failed" << std::endl;
Disconnect();
return;
}
_status = Status::CONNECTED;
}
// 重连
void Reconnect()
{
_status = Status::CONNECTING;
int count = 0;
while (count < _max_retries)
{
std::cout << "Reconnect : " << count + 1 << std::endl;
Connect();
if (_status == Status::CONNECTED)
{
return;
}
sleep(_retry_interval);
++count;
}
_status = Status::CLOSED;
}
//释放socket
void Disconnect()
{
if (_sockfd > 0)
{
close(_sockfd);
_sockfd = -1;
_status = Status::CLOSED;
}
}
//进行读写
void Process()
{
// echo client
std::string message;
while (true)
{
char inbuffer[1024];
std::cout << "input message: ";
std::getline(std::cin, message);
int n = ::write(_sockfd, message.c_str(), message.size());
if (n > 0)
{
int m = ::read(_sockfd, inbuffer, sizeof(inbuffer)); //这里使用recv()更好
if (m > 0)
{
inbuffer[m] = 0;
std::cout << inbuffer << std::endl;
}
else if (m == 0)
{
_status = Status::DISCONNECTED;
return;
}
else
{
_status = Status::CLOSED;
return;
}
}
else
{
_status = Status::CLOSED;
return;
}
}
}
Status GetStatus()
{
return _status;
}
private:
std::string _serverip; // 服务端ip
uint16_t _serverport; // 服务端端口号
int _sockfd; //套接字
int _retry_interval; // 重试时间间隔
int _max_retries; // 重试次数
Status _status; //状态描述
};
class TcpClient
{
public:
TcpClient(uint16_t serverport, const std::string &serverip) : _conn(serverip,serverport)
{
}
void Execute()
{
while (true)
{
//根据状态来调用不同函数
switch (_conn.GetStatus())
{
case Status::NEW:
_conn.Connect();
break;
case Status::CONNECTED:
std::cout << "连接成功, 开始进行通信." << std::endl;
_conn.Process();
break;
case Status::DISCONNECTED:
std::cout << "连接失败或者对方掉线,开始重连." << std::endl;
_conn.Reconnect();
break;
case Status::CLOSED:
_conn.Disconnect();
std::cout << "重连失败, 退出." << std::endl;
return; // 退出
default:
break;
}
}
}
~TcpClient()
{
}
private:
ClientConnection _conn; // 简单组合起来即可
};
TcpCilent.cc
#include"TcpServer.hpp"
int main()
{
TcpServer tp(8787);
tp.InitServer();
tp.Start();
return 0;
}