目录
一. 端口号的概念
二. 对于UDP和TCP协议的认识
三. 网络字节序
3.1 字节序的概念
3.2 网络通信中的字节序
3.3 本地地址格式和网络地址格式
四. socket编程的常用函数
4.1 sockaddr结构体
4.2 socket编程常见函数的功能和使用方法
五. UDP协议实现网络通信
5.1 UDP协议服务端的封装
5.2 UDP协议客户端的封装
六. TCP协议实现网络通信
6.1 TCP协议服务端的封装
6.2 TCP协议客户端的封装
七. 总结
进行网络通信,其根本目的并不是让两个主机之间进行通信,而是让运行在主机上的两个进程之间相互通信。如,我们在日常生活中经常要通过微信发送消息,我发送的消息必须要经过网络传输,才能够被对方接受。我发送的消息,并没有被对方主机的其他应用接受,而只是被对方主机上运行的微信这一进程接受。由此可见,网络通信本质上是网络中的两主机通过网络实现进程间通信,为此,OS必须通过特定的方式,来标识接受数据的进程。
端口号,是在某一主机上,用来标识进程唯一性的编号,与之对应的IP地址,用于表示网络中唯一的一台主机,因此,IP地址 + 端口号,可用于表示全网中唯一的一个进程。
关于端口号,有如下的基本结论:
端口号(port)和进程pid之间的关系:每个进程都有对应的pid,用于在系统中标识特定进程,但不进行网络通信的进程不需要有端口号,理论上讲id + port,也可以识别网络中唯一一个进程,但是使用端口号和id,能够实现系统进程和网络通信功能之间的解耦。
UDP协议,即用户数据报协议(User Datagram Protocol),其特征有:
TCP协议,即传输控制协议(Transmission Control Protocol),其特征有:
对于需要连接和不需要连接的理解:需要链接,类似于生活中的接打电话,我们给一方打电话的时候,对方需要听到电话铃声,确认接听才能通信,确认接听电话,就类似于网络通信中的建立链接。不需要连接,类似于生活中发送电子邮件的通信方式,我们要给某人发送电子邮件时,可以不用事先通知对方,只要发送,等待对方合适的时候查看即可,对方不需要事先准备接收邮件,即不需要连接。
对于可靠传输和不可靠传输的理解:可靠传输和不可靠传输并不是好坏的评判标准,原因是:(a). UDP协议虽然可能存在丢包失帧等问题,但是发生问题的概率极小,有些时候这并不是不可以接受的。 (b). 虽然TCP协议不会出现UDP这样的不可靠传输的问题,但是可靠通信的建立,是需要成本的,在有些可以一定程度接受数据传输出现问题的场景,采用TCP协议综合效益并不高。
内存中存储数据的字节序有两种:(1). 小端字节序 -- 低位存储在低地址,高位存储在高地址。(2). 大端字节序 -- 低位存储在高地址,高位存储在低地址。
图3.1以十六进制表示的数据int num = 0XAABBCCDD为例,展示了大端机和小端机存储数据的规则,这个数据第低位为DD,高位为AA。
假设这样一种场景,一台小端机要通过网络给一台大端机发送数据,假设他们以他们各自的字节序发送向网络中发数据和从网络中读数据,那么就会出现“乱序”问题,因此需要一定的协议,用于规范网络数据的字节序,以避免“乱序问题”。
规定:网络中的数据,全部采用大端字节序。
我们有时候无法确定发送的数据,或者从网络中读取来的数据是大端还是小端,为了保证发送和读取数据的可靠性,C标准库提供了下面4个函数,可以实现网络和主机数据之间的相互转换:
一般我们在主机中标识ip地址,都采用const char*数据类型、点分十进制方法来表示, 如1.23.122.234,但是在网络中,为了节省资源,ip应当采用四字节的方法来表示,下面几个函数的功能,是实现本地const char*点分十进制ip格式和网络四字节ip格式之间的转化:
注意:一般建议使用inet_ntop函数,而不是采用inet_ntoa函数,因为inet_ntoa为了返回本地格式的ip,会在函数内部开辟一块static空间来记录转换来的结果,这样就带来两个问题:1. 线程不安全 2. 如果多次调用inet_ntoa函数,那么最后一次调用的返回结果会覆盖掉前面的结果。而采用inet_ntop函数,返回结果会被存储在用户指定的buffer空间中,杜绝了inet_ntoa函数的这两个问题。
inet_ntop函数的af参数表示通信方式(AF_INET表示ipv4格式网络地址,AF_INET6表示ipv6格式网络地址),src为指向struct sin_addr类型数据的指针,dfs为接收结果的输出型参数。
图4.1给出了sockaddr、sockaddr_in和sockaddr_un的结构,其中sockaddr为socket API抽象出来的一种结构体,使用与ipv4、ipv6、udp、tcp、本地通信,等各种形式的socket。
sockaddr_in为网络通信使用的结构体,sockaddr_un为本地通信使用的结构体。
网络通信常用的结构为sockaddr_in,其内容包括:16位地址类型AF_INIT,用于确定通信方式为网络通信、32位IP地址用于在网络中定位特定的主机、8字节填充没有实际意义,一般为0。
struct sockaddr_in的定义:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct in_addr的定义:
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
对于socket程序,一般包含四个头文件,即可支持所有的socket相关函数:
创建socket文件描述符函数 -- socket:
socket函数 -- 创建socket文件描述符
函数原型:int socket(int domain, int type, int protocol)
函数参数:
- domain -- 选取通信的范围(网络还是本地),AF_INET为网络通信,AF_LOCAL为本地通信。
- type -- 通信类型,UDP协议传SOCK_DGRAM,TCP协议传SOCK_STREAM。
- protocal -- 指定协议类型,一般传0即可。
返回值:如果成功,返回创建的socket文件描述符,如果失败返回-1。
绑定端口号-- bind:
bind函数 -- 绑定端口号
函数原型:int bind(int socket, const struct sockaddr *address, socklen_t *len)
函数参数:
- socket -- socket文件描述符
- address -- 输入型参数,用于指定套接字sockaddr
- len -- 输入型参数,用于给出sockaddr结构体的长度(占用多少字节)
返回值:成功返回0,失败返回-1。
设置监听状态 -- listen:
listen函数 -- 设置监听状态
函数原型:int listen(int socket, int backlog)
函数参数:
- socket -- socket文件描述符
- backlog -- 应传一个不太大也不太小的数字。
返回值:成功返回0,失败返回-1。
建立通信连接 -- connect:
connect函数 -- 建立通信连接
函数原型:int connect(int socket, struct sockaddr *address, socklen_t len)
函数参数:
- socket -- socket文件描述符
- address -- 被链接的一端的套接字
- len -- 套接字长度
返回值:成功返回0,失败返回-1。
接受通信另一方的连接请求 -- accept:
accept函数 -- 接受通信另一端的连接请求
函数原型:int accept(int socket, struct sockaddr_in *address, socklen_t *len)
函数参数:
- socket -- socket文件描述符。
- address -- 输出型参数,用于获取请求连接的一方的套接字信息。
- len -- 套接字长度。
返回值:成功返回用于通信的“网络文件”的文件描述符,失败返回-1。
从网络中读取数据函数 -- recvfrom:
recvfrom函数 -- UDP协议从网络中读取数据
函数原型:ssize_t recvfrom(int socket, void *buffer, size_t length, int flag, struct sockaddr *addr, socklen_t *addr_length)
函数参数:
- socket -- socket文件描述符。
- buffer -- 接受数据的缓冲区。
- length -- 至多读取的字节数。
- flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。
- addr -- 输出型参数,用于接受发送数据的主机ip及进程端口号。
- addr_length:套接字长度。
返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。
从网络中读取数据 -- recv:
recvfrom函数 -- TCP协议从网络中读取数据
函数原型:ssize_t recv(int socket, void *buffer, size_t length, int flag)
函数参数:
- socket -- socket文件描述符。
- buffer -- 接受数据的缓冲区。
- length -- 至多读取的字节数。
- flag -- 读取数据的方式,一般设置为0,表示阻塞式等待网络中被写入数据。
返回值:读取成功返回读到的字节数,对端关闭返回0,失败返回-1。
向网络中发送数据函数 -- sendto:
sendto函数 -- 向网络中发送数据
函数原型:ssize_t sendto(int socket, void *buffer, size_t length, int flag, const struct sockaddr *addr, socklen_t addr_length)
函数参数:
- socket -- socket文件描述符。
- buffer -- 存储待发送数据的缓冲区。
- length -- 要发送的数据的字节数。
- flag -- 发生数据的方式,一般直接设置为0即可。
- addr -- 指定接受数据的主机ip和端口号。
- addr_length:套接字长度。
返回值:成功返回发送出去的字节数,失败返回-1。
本文实现一个服务端的demo程序,其功能为:服务端从客户端读取数据,记录数据源主机的ip和端口号,如果源主机第一次向服务器发送数据,就将该主机的ip和端口号插入到哈希表中,每次服务器接受到数据,就将数据发回哈希表中记录的主机,这样就模拟实现了简单的群聊功能。
服务端初始化步骤:创建socket文件描述符 -> 绑定端口号。
服务端启动后的工作流程:通过recvfrom函数从客户端读取数据 -> 判断发送数据的客户端是否已经向服务器发送过数据,如果没有,那么将客户端的ip和端口号插入哈希表 -> 将读取到的数据发回哈希表中记录的客户端。
启动服务端程序时,要指定端口号,以便客户端能够与服务端建立通信,一般来说服务端不用显示设定ip,而是通过INADDR_ANY来设置,这表示无论客户端ip是多少,都可以实现与服务端的通信。
Log.hpp文件(日志打印相关内容):
#pragma once
#include
#include
#include
#include
#define DEBUG 0
#define NORMAL 1
#define WARING 2
#define ERROR 3
#define FATAL 4
const char* g_levelMap[5] =
{
"DEBUG",
"NORMAL",
"WARING",
"ERROR",
"FATAL"
};
void logMessage(int level, const char *format, ...)
{
// 1. 输出常规部分
time_t timeStamp = time(nullptr);
struct tm *localTime = localtime(&timeStamp);
printf("[%s] %d-%d-%d, %02d:%02d:%02d\n", g_levelMap[level], localTime->tm_year, localTime->tm_mon, \
localTime->tm_mday, localTime->tm_hour, localTime->tm_min, localTime->tm_sec);
// 2. 输出用户自定义部分
va_list args;
va_start(args, format);
vprintf(format, args);
va_end(args);
}
udp_serve.hpp文件(对服务端封装):
#pragma once
#include "Log.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Server
{
public:
// 服务端构造函数
Server(uint16_t port, const std::string& ip = "")
: _port(port)
, _ip(ip)
, _sock(-1)
{ }
// 初始化服务器
void init()
{
// 1. 创建网络套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if(_sock < 0) // 检查套接字创建成功与否
{
logMessage(FATAL, "%d:%s\n", errno, strerror(errno));
exit(2);
}
logMessage(DEBUG, "套接字创建成功, _sock:%d\n", _sock);
// 2. bind:将用户设置的ip和port在内核中与进程相关联
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
// INADDR_ANY: 表示服务器在工作过程中可以从任意ip获取数据
// inet_addr函数: 将主机ip转为4字节网络ip
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
local.sin_port = htons(_port); // 将主机端口转换为网络端口格式
if(bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "%d:%s\n", errno, strerror(errno));
exit(3);
}
logMessage(DEBUG, "用户设置的ip和port在内核中与进程相关联成功!\n");
}
// 启动服务器程序
void start()
{
// 服务器进程永不退出
// 从客户端读取数据
char buffer[1024]; // 输出缓冲区
char key[128]; // 存储客户端ip和端口号
struct sockaddr_in sock_cli; // 客户端套接字
memset(&sock_cli, 0, sizeof(sock_cli)); // 初始化0
socklen_t len = sizeof(sock_cli); // 输入型参数 -- 套接字长度
std::string addr_cli; // 数据源客户端的ip
uint16_t port_cli; // 数据源客户端的端口号
while(true)
{
// 输出读取到的数据
memset(buffer, 0, sizeof(buffer));
memset(key, 0, sizeof(key));
ssize_t n = recvfrom(_sock, (void *)buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&sock_cli, &len);
if(n > 0)
{
buffer[n] = 0; // 添加尾部'/0'
addr_cli = inet_ntoa(sock_cli.sin_addr); // inet_ntoa函数负责将网络ip转换为主机ip
port_cli = ntohs(sock_cli.sin_port); // 网络套接字转换为主机套接字
snprintf(key, 128, "[%s-%d]", addr_cli.c_str(), port_cli);
printf("[%s:%d]# %s\n", addr_cli.c_str(), port_cli, buffer); // 输出发送端的ip和port,以及发送的内容
}
else if(n == 0)
{
logMessage(DEBUG, "未读取到数据!\n");
continue;
}
else // 数据读取失败
{
logMessage(ERROR, "读取数据失败!\n");
continue;
}
// 将客户端的ip和port插入到哈希表
if(_mp.find(key) == _mp.end())
{
_mp.insert({key, sock_cli});
logMessage(NORMAL, "成功插入客户端, %s\n", key);
}
// 将读取到的数据全部发送给客户端主机
for(const auto& iter : _mp)
{
std::string msg_cli;
msg_cli += key;
msg_cli += "# ";
msg_cli += buffer;
if(sendto(_sock, msg_cli.c_str(), msg_cli.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second)) < 0)
{
logMessage(ERROR, "服务器发回消息失败!");
continue;
}
logMessage(NORMAL, "向客户端写回数据 -- %s\n", iter.first.c_str());
}
}
}
// 析构函数
~Server()
{
if(_sock >= 0)
{
close(_sock);
}
}
private:
uint16_t _port; // 端口号
std::string _ip; // 服务器ip地址
int _sock; // 套接字对应文件描述符
std::unordered_map _mp; // 哈希表,记录接收到信息的客户端的ip和port
};
udpserve.cc文件(服务端源文件):
#include "udp_serve.hpp"
#include
void usage(const char *command)
{
std::cout << "\nUsage# " << command << " port\n" << std::endl;
}
int main(int argc, const char **argv)
{
if(argc != 2)
{
usage(argv[0]);
exit(1);
}
uint16_t port = static_cast(atoi(argv[1]));
std::unique_ptr psvr(new Server(port));
psvr->init();
psvr->start();
return 0;
}
本文采用多线程的方式实现UDP客户端demo程序,一个线程负责从服务器读取数据,另一个线程负责向服务器发送数据。
客户端初始化init:创建socket即可,不需要bind端口号,当客户端第一次向服务端发送数据的时候,OS会自动为客户端进程分配端口号。
客户端启动函数执行的工作:创建两个线程,一个调用recvfrom函数从服务端读数据,另一个调用sendto函数向服务器写数据。
启动客户端程序时,需要告知服务器对应的ip和端口号,才能够成功与服务器建立通信。
udp_client.hpp文件(封装客户端):
#pragma once
#include "Log.hpp"
#include "Thread.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct SendMessageData
{
int _sock;
struct sockaddr_in _sock_srv;
socklen_t _len;
SendMessageData(int sock, struct sockaddr_in sock_srv)
: _sock(sock), _sock_srv(sock_srv), _len(sizeof(sock_srv))
{
}
};
class Client
{
public:
// 构造函数
Client(const std::string &ip, uint16_t port)
: _ip(ip), _port(port), _sock(-1)
{
}
// 初始化函数
void init()
{
// 1. 创建网络套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(2);
}
// 2. 绑定 -- 客户端需要绑定,但一般不会由程序员来进行绑定
// 而是在第一次发送消息时,由OS自动分配ip和port
}
// 客户端启动程序
void start()
{
// 创建服务器的struct sockaddr
struct sockaddr_in srv_sock;
memset(&srv_sock, 0, sizeof(srv_sock));
srv_sock.sin_family = AF_INET;
srv_sock.sin_addr.s_addr = inet_addr(_ip.c_str()); // 主机ip转为网络ip
srv_sock.sin_port = htons(_port); // 主机port转为网络port
SendMessageData sendData(_sock, srv_sock);
// 发送消息
thread th_send(send_message, (void *)&sendData);
th_send.start();
// 接受反馈回来的消息
thread th_recieve(recieve_message, (void *)&_sock);
th_recieve.start();
th_send.join();
th_recieve.join();
}
// 析构函数
~Client()
{
if (_sock < 0)
{
close(_sock);
}
}
private:
static void *send_message(void *args)
{
SendMessageData *ptr = (SendMessageData *)args;
while (true)
{
std::string msg;
std::cerr << "请输入你要发送的消息: " << std::flush;
std::getline(std::cin, msg); // 按行读取
sendto(ptr->_sock, msg.c_str(), msg.size(), 0, (const sockaddr *)&ptr->_sock_srv, ptr->_len);
}
return nullptr;
}
static void *recieve_message(void *args)
{
// memset(buffer, 0, sizeof(buffer));
char buffer[1024];
while (true)
{
struct sockaddr tmp;
memset(&tmp, 0, sizeof(tmp));
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(*(int *)args, (void *)buffer, sizeof(buffer) - 1, 0, (sockaddr *)&tmp, &len);
if (n > 0)
{
// std::cerr << "aaaa" << std::endl;
buffer[n] = '\0';
printf("%s\n", buffer);
}
}
return nullptr;
}
std::string _ip; // 发生数据的主机ip
uint16_t _port; // 端口号
int _sock; // 套接字
};
udp_client.cc文件(客户端源文件):
#include "udp_client.hpp"
#include
#include
void Usage(const char *command)
{
std::cout << "/nUsage: " << command << " ip port\n" << std::endl;
}
int main(int argc, const char **argv)
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
// signal(SIGCHLD, SIG_IGN);
std::unique_ptr pcli(new Client(argv[1], atoi(argv[2])));
pcli->init();
pcli->start();
return 0;
}
本文采用多进程的方式来编写服务端demo代码,每次接收到客户端的连接请求,就为这个客户端创建一个进程,用于与该客户端通信。
服务端初始化init:创建socket套接字 -> 将本地ip和端口号在内核中与当前进程绑定 -> 设置服务端进程处于listen状态,以便随时接受客户端的连接请求。
服务端启动start:接受客户端的连接请求并记录请求连接的客户端的套接字 -> 创建子进程 -> 在子进程中调用读取客户端发送的信息 -> 读取成功后,发回给客户端。
启动服务器时,需要显示给出端口号,以便客户端能够顺利连接到服务器。
tcp_server.hpp文件(服务端封装):
#pragma once
#include "Log.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const int g_size = 1024;
static void server(int serverSock, struct sockaddr_in sock_cli)
{
char buffer[g_size]; // 存储读取数据的缓冲区
while(true)
{
ssize_t n = recv(serverSock, buffer, g_size - 1, 0); // 读取数据
if(n > 0)
{
buffer[n] = '\0';
uint16_t cli_port = ntohs(sock_cli.sin_port);
char cli_addr[20]; // 地址
memset(cli_addr, 0, sizeof(cli_addr));
// socklen_t len = sizeof(sock_cli.sin_addr);
inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, sizeof(cli_addr));
printf("[%s-%d] %s\n", cli_addr, cli_port, buffer);
// 发回客户端
send(serverSock, buffer, strlen(buffer), 0);
}
else if(n == 0)
{
logMessage(DEBUG, "对端关闭,读取结束!\n");
break;
}
else // n < 0
{
logMessage(ERROR, "读取失败!\n");
break;
}
}
close(serverSock);
}
class TcpServer
{
public:
TcpServer(uint16_t port, const std::string &ip = "")
: _port(port), _ip(ip), _listenSock(-1)
{ }
// 服务器初始化
void init()
{
// 1. 创建网络套接字
_listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0) // 创建socket失败
{
logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "socket success, _listenSock:%d\n", _listenSock);
// 2. 绑定端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // 设置网络协议族
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // 本地格式地址转为网络格式
local.sin_port = htons(_port); // 本地格式端口号转为网络格式
if (bind(_listenSock, (const sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(FATAL, "bind error, %d:%s\n", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "bind success, %s:%d\n", _ip.c_str(), _port);
// 3. 设置监听状态
if (listen(_listenSock, _backlog) < 0)
{
logMessage(FATAL, "listen error, %d:%s\n", errno, strerror(errno));
exit(4);
}
logMessage(NORMAL, "listen success\n");
logMessage(NORMAL, "Server Init Success!\n");
}
// 运行服务器
void start()
{
while (true)
{
// 接受客户端的链接请求
struct sockaddr_in sock_cli;
memset(&sock_cli, 0, sizeof(sock_cli));
socklen_t len = sizeof(sock_cli);
int serverSock = accept(_listenSock, (struct sockaddr *)&sock_cli, &len);
if (serverSock < 0) // 接受客户端请求失败
{
logMessage(ERROR, "accept error, %d:%s\n", errno, strerror(errno));
continue;
}
uint16_t cli_port = ntohs(sock_cli.sin_port);
char cli_addr[20]; // 地址
memset(cli_addr, 0, sizeof(cli_addr));
inet_ntop(AF_INET, &sock_cli.sin_addr, cli_addr, len);
cli_addr[strlen(cli_addr)] = '\0';
logMessage(NORMAL, "accept success [%s-%d]\n", cli_addr, cli_port);
// 多进程接受客户端信息
pid_t id = fork();
if (id == 0)
{
if (fork() > 0)
exit(0); // 子进程退出
// 子进程的子进程(孙子进程)此时变为孤儿进程
// 由1号进程领养,OS自动回收进程
server(serverSock, sock_cli);
exit(0);
}
waitpid(id, nullptr, 0);
close(serverSock);
}
}
// 析构函数
~TcpServer()
{
if (_listenSock >= 0)
{
close(_listenSock);
}
}
private:
uint16_t _port; // 端口号
std::string _ip; // 本地ip
int _listenSock; // socket文件描述符
static const int _backlog = 20;
};
tcp_server.cc文件(服务端源文件):
#include "tcp_server.hpp"
#include
void Usage(const char *proc)
{
std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}
int main(int argc, const char **argv)
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
std::unique_ptr ptr_srv(new TcpServer(atoi(argv[1])));
ptr_srv->init();
ptr_srv->start();
return 0;
}
TCP协议客户端,需要先调用connect函数,尝试与服务器建立链接,才能与服务器正常通信。
运行TCP协议客户端的时候,需要显示给的IP地址和服务器对应的端口号。
tcp_client.hpp文件(客户端封装):
#pragma once
#include "Log.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class TcpClient
{
public:
TcpClient(const std::string& ip, uint16_t port)
: _ip(ip), _port(port), _sock(-1)
{ }
// 初始化客户端
void init()
{
// 创建socket文件描述符
_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_sock < 0)
{
logMessage(FATAL, "socket error, %d:%s\n", errno, strerror(errno));
exit(2);
}
logMessage(NORMAL, "socket success, _sock:%d\n", _sock);
// 客户端与服务端连接
struct sockaddr_in sock_srv;
memset(&sock_srv, 0, sizeof(sock_srv));
sock_srv.sin_family = AF_INET;
sock_srv.sin_addr.s_addr = inet_addr(_ip.c_str());
sock_srv.sin_port = htons(_port);
socklen_t len = sizeof(sock_srv);
if(connect(_sock, (struct sockaddr *)&sock_srv, len) < 0)
{
logMessage(FATAL, "connect error, %d:%s\n", errno, strerror(errno));
exit(3);
}
logMessage(NORMAL, "connect success\n");
logMessage(NORMAL, "Client Init Success\n");
}
void start()
{
std::string msg; // 发送的消息
char buffer[1024]; // 接受服务器发回的消息
while(true)
{
std::cout << "请输入你要发送的消息: " << std::flush;
std::getline(std::cin, msg);
if(msg == "quit")
break;
ssize_t n = send(_sock, msg.c_str(), msg.size(), 0);
if(n > 0)
{
logMessage(NORMAL, "成功发送数据!\n");
ssize_t s = recv(_sock, buffer, 1023, 0);
if(s > 0)
{
buffer[s] = '\0';
printf("回显# %s\n", buffer);
}
else if(s == 0)
{
logMessage(DEBUG, "服务器退出!\n");
break;
}
else
{
logMessage(DEBUG, "获取服务器发回数据失败!\n");
continue;
}
}
}
}
~TcpClient()
{
if(_sock < 0)
{
close(_sock);
}
}
private:
std::string _ip;
uint16_t _port;
int _sock;
};
tcp_client.cc文件(客户端源文件):
#include "tcp_server.hpp"
#include
void Usage(const char *proc)
{
std::cout << "\nServer Usage# " << proc << " ServerPort\n" << std::endl;
}
int main(int argc, const char **argv)
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
std::unique_ptr ptr_srv(new TcpServer(atoi(argv[1])));
ptr_srv->init();
ptr_srv->start();
return 0;
}