文章目录
网络通信的本质
网络之间的通信,本质上就是进程间通信
对双方主机的两个进程而言,需要先将数据发送到对方的主机(ip地址),再找到指定的进程(port:端口号),就能实现通信
ip地址用来标识互联网中唯一的一台主机;port端口号用来标识该指定机器中进程的唯一性
那么(ip, port) 则可以用来表示互联网中唯一一个进程,ip + port 也叫网络套接字 socket
如何理解port:
一个端口号和一个进程相绑定,一个进程可以绑定多个端口号,反之则不可以。
那么为什么不用进程pid来表示网络中进程的唯一性呢?
为了其他的进程模块和网络进行解耦(万一pid的规则变化,网络部分也不受影响),port是专门用于网络通信的
tcp 和 udp 协议
tcp 协议常用于可靠通信,适用于对数据要求比较高的场景(如游戏,传输重要文件等),复杂
udp 协议用于不可靠通信,适用于允许数据偶尔出现差错的场景(如体育赛事直播等),简单,快
这两个协议没有好坏之分,只是应用场景不同,如果不确定使用哪个的时候,就要 tcp,毕竟复杂一点比丢包好
网络字节序
机器有大小端之分,大小端机器存储数据方式不同。大端是“正着存储”的,可读性较好,因此在网络传输时规定,所以到达网络的数据,必须时大端存储的,因此,如果是小端机,收发数据到网络时需要先转化为大端
网络主机数据转化接口
ip 地址为4个字节,使用 uint32_t,port 为2个字节,使用 uint_16
htonl、htons 是转网络,ntohl、ntohs 是转主机数据,使用这些接口可以自动识别机器的大小端,并将数据转化为需要的大小端数据。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uintl6_t ntohs(uint16_t netshort);
socket 创建 udp 套接字接口
// 创建 socket 文件描述符 (tcp/udp, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
int socket(af_inet, sock_dgram, 0); // 创建网络套接字, 成功返回文件描述符, 失败返回 -1
// af_inet 说明协议家族使用的是ipv4, sock_dgram表示当前创建的是 udp套接字,本质是创建文件
// 绑定端口号 (tcp/udp, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
填充 sockaddr_in 结构(套接字信息)
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = af_inef; // 设置协议家族
local.sin_port = htons(_port); // 将端口号转化为网络字节序列并赋值给 sockaddr_in 参数
local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 使用 inet_addr 接口,变为4字节网络序列
服务器收发消息
recvform(); //收消息
sendto(); //发消息
查看网络状态 可以查看启动的服务器的信息
sudo netstat -anup
udp 通信
首先,要有一个客户端,一个服务端。这两台机器的通信可以看作是两个在这台机器上的进程进行
通信,udp 是全双工通信,因此两个进程可以互发消息,不会相互影响。
服务端逻辑
服务端的主体逻辑可以封装成一个类对象,主函数中通过调用类中的 init 函数和 start 函数来实现服务器的初始化和启动。
首先,init 类内函数中,先创建 socket 套接字,再填充 sockaddr_in 结构,sockaddr_in是网络通信相关的结构体,有 ip 地址,端口号,协议家族这几个字段,然后将 socket 套接字和 sockaddr_in 结构体 bind 绑定(注意,不是c++那个bind),绑定后,所谓的互联网中唯一的进程就出现了!
然后 start 类内函数中,设置一个死循环,定义一个缓冲区,并定义一个 sockaddr_in 结构体变量,sockaddr_in 结构体变量用来存储收到的消息的网络地址,缓冲区用于存放收到的消息。通过 recvfrom 函数收到来自另一个互联网中唯一进程的消息后,如果 recvfrom 函数接受成功,就可以对消息做处理,然后通过 sendto 函数,将消息转发给对面的主机,对面主机的网络地址,就是 recvfrom 函数收到的存储在 sockaddr_in 结构体中的网络地址!
客户端逻辑
客户端的逻辑比较简单,就是拿到服务端的 ip 地址和端口号后,和服务端进行通信。如果通信的形式是收->发->收->发->收…这种收到消息后再发消息,发消息后才能收消息的情况,那么只需要定义一个死循环即可,先发消息,然后检测是否发送成功,成功了才收消息这种的
![[pasted image 20240608100519.png]]
也可以使用多线程,一个线程只负责死循环发消息,一个线程只负责死循环收消息,即可实现 qq、微信式通信
![[pasted image 20240608100201.png]]
这大概就是udp通信写程序的基本逻辑
tcp 通信
tcp 套接字
tcp 通信与 udp不同的是,tcp要先建立连接
才能通信,因此与 dup 服务端不同的是,tcp要有一个 listensock与本地网络信息绑定,还要有一个普通socket 来与 client 通信
服务端程序编写步骤
- 创建
listensocket
, 它是 file fd, 本质是文件描述符 - 填充本地网络信息(初始化 struct sockaddr_in)并bind
listensocket
和本地网络信息 - 设置socket为监听状态,tcp特有 listen(_listensock, default_backlog)
- 获取连接,用 listensocket 来获取连接,定义一个 socket 用来接收客户端的信息,再定义一个网络套接字信息,accept 接收成功后,socket 将自动与网络信息绑定
- 提供服务,建立连接成功后,用 read 读 socket,即可接收客户端信息,用 write 往 socket 中写,即可向客户端发送信息
- 规定一个端口号,就能启动服务器
// 获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listensock, struct sockaddr*(&peer), &len);
// 填充网络信息时,ip地址的处理优化:
// p:process(进程), n(网络) -- 不太准确,但是好记忆
inet_pton(af_inet, serverip.c_str(), &server.sin_addr); // 1. 字符串ip->4字节ip 2. 网络序列
客户端程序编写步骤
- 先用服务端的ip 地址和它开放的端口号启动客户端
- 创建 socket 并初始化
- 填写 struct sockaddr_in 套接字信息
- 用 sockfd 建立连接 connect
- 收发信息用 read 和 write 向 sockfd 读写就行
如果要与多个客户端建立通信,则可使用多进程的方式:每检测到和一个客户端建立连接,就 fork 一个子进程,让子进程去与这个新建立的客户端通信(子进程继承父进程的文件描述符,但每个进程的文件描述符之间是独立的)
当然,也可以采用多线程,只需要将次级进程设置为分离即可!
两种通信程序代码
udp服务端程序编写
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h> /* see notes */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <functional>
#include <pthread.h>
#include <strings.h>
#include <unistd.h>
#include "nocopy.hpp"
#include "comm.hpp"
#include "log.hpp"
#include "inetaddr.hpp"
#include "threadpool.hpp"
using task_t = std::function<void()>;
const static int defaultport = 8888;
const static int defaultfd = -1;
// using func_t = std::function<std::string(std::string)>; // 定义了一个参数为 string, 返回值也为 string 的函数
// 继承nocopy, 防止udpserver 被拷贝,因为基类中已经删除了拷贝构造和赋值
class udpserver : public nocopy
{
public:
udpserver(u_int16_t port = defaultport)
: _port(port), _sockfd(defaultfd)
{
pthread_mutex_init(&_user_mutex, nullptr);
}
void init() // 初始化服务器
{
// 1. 创建 socket 文件细节
_sockfd = socket(af_inet, sock_dgram, 0);
std::cout << "sockfd: " << _sockfd << std::endl;
if (_sockfd < 0)
{
lg.logmessage(fatal, "sock errr: %d %s\n", errno, strerror(errno));
exit(socket_err);
}
lg.logmessage(info, "sock success, sockfd: %d\n", _sockfd);
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = af_inet;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inaddr_any; // ip 设置为 0
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // inet_addr 可以将点分十进制的ip地址转化为四字节网络字节序列
int n = ::bind(_sockfd, (struct sockaddr*)(&local), (socklen_t)sizeof(local)); // 绑定到内核中, 成功就返回 0
if (n != 0)
{
lg.logmessage(fatal, "bind errr: %d %s\n", errno, strerror(errno));
exit(bind_err);
}
threadpool<task_t>::getinstance()->start();
}
void map()
{
// 添加用户即可 ..
}
void addonlineuser(inetaddr addr) // 添加用户
{
lockguard lockguard(&_user_mutex);
for (auto& user : _online_user)
{
if (addr == user) return;
}
_online_user.push_back(addr);
lg.logmessage(debug, "%s : %d is add onlineusers!\n", addr.ip().c_str(), addr.port());
}
void route(int sockfd, const std::string& message)
{
// 先加锁,然后转化给所有人
lockguard lockguard(&_user_mutex);
for (auto& user : _online_user)
{
lg.logmessage(debug, "send to %s %d success!\n", user.ip().c_str(), user.port());
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&user.getaddr(), (socklen_t)sizeof(user.getaddr()));
}
}
void start()
{
char buffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if (n > 0)
{
inetaddr addr(peer);
buffer[n] = 0;
addonlineuser(addr);
std::string message;
// 检测是否加入
if (_hash.count(addr.ip()) > 0) message = _hash[addr.ip()] + buffer;
else message = "[" + addr.ip() + ":" + std::to_string(addr.port()) + "]# " + buffer;
std::cout << "message:" << message << std::endl;
task_t task = std::bind(&udpserver::route, this, _sockfd, message);
threadpool<task_t>::getinstance()->push(task);
}
}
}
~udpserver()
{
pthread_mutex_destroy(&_user_mutex);
}
private:
u_int16_t _port;
int _sockfd;
// func_t _onmessage;
std::vector<inetaddr> _online_user;
pthread_mutex_t _user_mutex;
std::unordered_map<std::string, std::string> _hash;
};
udp 客户端程序编写
#include <string>
#include <iostream>
#include <cstring>
#include <sys/types.h> /* see notes */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "comm.hpp"
#include "log.hpp"
#include "inetaddr.hpp"
#include "pthread.hpp"
void usage(std::string proc)
{
std::cout << "usage:" << proc << " server_ip server_port" << std::endl;
}
class threaddata
{
public:
threaddata(int sock, struct sockaddr_in& server)
:_sockfd(sock), _serveraddr(server)
{}
~threaddata()
{}
public:
int _sockfd;
inetaddr _serveraddr;
};
// 负责收消息
void recverroutine(threaddata& td)
{
while (true)
{
char buffer[4096];
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t m = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
}
// 负责发消息
void senderroutine(threaddata& td)
{
while (true)
{
std::string inbuffer;
std::getline(std::cin, inbuffer);
auto server = td._serveraddr.getaddr();
ssize_t n = sendto(td._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&server, sizeof server);
if (n <= 0) std::cout << "send erro......" << std::endl;
usleep(50);
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
return usage_err;
}
std::string server_ip = argv[1];
u_int16_t port = std::stoi(argv[2]);
// 创建 socket
int sockfd = socket(af_inet, sock_dgram, 0);
if (sockfd < 0)
{
lg.logmessage(fatal, "sock errr: %d %s\n", errno, strerror(errno));
return socket_err;
}
lg.logmessage(info, "sock success, sockfd: %d\n", sockfd);
// lient 需要bind,但是不需要显示bind,让本地os自动随机bind,选择随机端口号/
// 填充 server 信息, 方便后面收发信息使用
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = af_inet;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
server.sin_port = htons(port);
threaddata td(sockfd, server);
thread<threaddata> recver("recver", recverroutine, td);
thread<threaddata> sender("sender", senderroutine, td);
recver.start();
sender.start();
recver.join();
sender.join();
close(sockfd);
return 0;
}
tcp 服务端程序编写
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <unordered_map>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include "log.hpp"
#include "nocopy.hpp"
#include "comm.hpp"
#include "inetaddr.hpp"
#include "threadpool.hpp"
class threaddata
{
public:
threaddata(int sock, tcpserver *ptr, struct sockaddr_in &peer)
:sockfd(sock), svr_ptr(ptr), addr(peer)
{}
int sockfd() {return sockfd;}
tcpserver *getserver() { return svr_ptr;};
~threaddata()
{
close(sockfd);
}
private:
int sockfd;
tcpserver* svr_ptr;
public:
inetaddr addr;
};
class tcpserver : public nocopy
{
public:
tcpserver(uint16_t port) : _port(port), _isrunning(false)
{
}
// 都是固定套路
void init()
{
// 1. 创建socket, file fd, 本质是文件
_listensock = socket(af_inet, sock_stream, 0);
if (_listensock < 0)
{
lg.logmessage(fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
exit(fatal);
}
lg.logmessage(debug, "create socket success, sockfd: %d\n", _listensock);
// 固定写法,解决一些少量的bind失败的问题 -- todo
int opt = 1;
setsockopt(_listensock, sol_socket, so_reuseaddr | so_reuseport, &opt, sizeof(opt));
// 2. 填充本地网络信息并bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = af_inet;
local.sin_port = htons(_port);
local.sin_addr.s_addr = htonl(inaddr_any);
// 2.1 bind
if (bind(_listensock, conv(&local), sizeof(local)) != 0)
{
lg.logmessage(fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
exit(bind_err);
}
lg.logmessage(debug, "bind socket success, sockfd: %d\n", _listensock);
// 3. 设置socket为监听状态,tcp特有的
if (listen(_listensock, default_backlog) != 0)
{
lg.logmessage(fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
exit(listen_err);
}
lg.logmessage(debug, "listen socket success, sockfd: %d\n", _listensock);
threadns::threadpool<task_t>::getinstance()->start();
funcs.insert(std::make_pair("defaultservice", std::bind(&tcpserver::defaultservice, this, std::placeholders::_1, std::placeholders::_2)));
}
void service(int sockfd, inetaddr addr)
{
char buffer[1024];
// 一直进行io
while (true)
{
ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << addr.printdebug() << "# " << buffer << std::endl;
std::string echo_string = "server echo# ";
echo_string += buffer;
write(sockfd, echo_string.c_str(), echo_string.size());
}
else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!)
{
lg.logmessage(info, "client quit...\n");
break;
}
else
{
lg.logmessage(error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
break;
}
}
}
// static void *handlerrequest(void *args) // this
// {
// pthread_detach(pthread_self());
// threaddata *td = static_cast<threaddata*>(args);
// td->getserver()->service(td->sockfd(), td->addr);
// delete td;
// return nullptr;
// }
void start()
{
_isrunning = true;
signal(sigchld, sig_ign); // 在linux环境中,如果对sig_ign进行忽略,子进程退出的时候,自动释放自己的资源
while (_isrunning)
{
// 4. 获取连接
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_listensock, conv(&peer), &len);
if (sockfd < 0)
{
lg.logmessage(warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
continue;
}
lg.logmessage(debug, "accept success, get n new sockfd: %d\n", sockfd);
// 5. 提供服务啊, v1~v4
// v1 单进程
// service(sockfd);
// close(sockfd);
// v2 多进程
// pid_t id = fork();
// if (id < 0)
// {
// close(sockfd);
// continue;
// }
// else if (id == 0)
// {
// // child
// close(_listensock);
// if (fork() > 0)
// exit(0);
// // 孙子进程,孤儿进程,被系统领养,正常处理
// service(sockfd);
// close(sockfd);
// exit(0);
// }
// else
// {
// close(sockfd);
// pid_t rid = waitpid(id, nullptr, 0);
// if (rid == id)
// {
// // do nothing
// }
// }
// v3 多进程 - 信号版
// pid_t id = fork();
// if (id < 0)
// {
// close(sockfd);
// continue;
// }
// else if (id == 0)
// {
// // child
// close(_listensock);
// service(sockfd);
// close(sockfd);
// exit(0);
// }
// else
// {
// close(sockfd);
// // 父进程不想等待呢?
// }
// v3 进程池 - 课堂不讲了 - 试一试 -- 问题?先创建子进程,然后才获取的连接,子进程看不到新获取的文件fd.
// v4 多线程
// threaddata *td = new threaddata(sockfd, this, peer);
// pthread_t tid;
// pthread_create(&tid, nullptr, handlerrequest, td);
// 主线程和新线程,不需要关闭所谓文件描述符, 将线程设置为分离
// v5 线程池 -- 不能让我们的服务器,提供长服务
task_t t = std::bind(&tcpserver::routine, this, sockfd, inetaddr(peer));
threadns::threadpool<task_t>::getinstance()->push(t);
}
}
std::string read(int sockfd)
{
char type[1024];
ssize_t n = read(sockfd, type, sizeof(type) - 1);
if (n > 0)
{
type[n] = 0;
}
else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!)
{
lg.logmessage(info, "client quit...\n");
}
else
{
lg.logmessage(error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
}
return type;
}
void routine(int sockfd, inetaddr addr)
{
funcs["defaultservice"](sockfd, addr);
std::string type = read(sockfd);
lg.logmessage(debug, "%s select %s\n", addr.printdebug().c_str(), type.c_str());
if (type == "ping")
funcs[type](sockfd, addr);
else if (type == "translate")
funcs[type](sockfd, addr);
else if (type == "transform")
funcs[type](sockfd, addr);
else
{
}
close(sockfd);
}
void defaultservice(int sockfd, inetaddr& addr)
{
(void)addr;
std::string service_list = " |";
for (auto func : funcs)
{
service_list += func.first;
service_list += "|";
}
write(sockfd, service_list.c_str(), service_list.size());
}
void registerfunc(const std::string& name, callback_t func)
{
funcs[name] = func;
}
~tcpserver()
{
}
private:
uint16_t _port;
int _listensock; // todo
bool _isrunning;
// 构建业务逻辑
std::unordered_map<std::string, callback_t> funcs;
};
tcp 客户端程序编写
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include "comm.hpp"
using namespace std;
void handler(int signo)
{
std::cout << "signo: " << signo << std::endl;
exit(0);
}
#define retry_count 5
void usage(const std::string &process)
{
std::cout << "usage: " << process << " server_ip server_port" << std::endl;
}
bool visitserver(std::string &serverip, uint16_t &serverport, int *cnt)
{
// 1. 创建socket
string inbuffer;
char service_list[1024];
ssize_t m = 0;
ssize_t n = 0;
int sockfd = socket(af_inet, sock_stream, 0);
if (sockfd < 0)
{
cerr << "socket error" << endl;
return false;
}
bool ret = true;
// 2. 要不要bind?必须要有ip和port, 需要bind,但是不需要用户显示的bind,client系统随机端口
// 发起连接的时候,client会被os自动进行本地绑定
// 2. connect
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = af_inet;
server.sin_port = htons(serverport);
// p:process(进程), n(网络) -- 不太准确,但是好记忆
inet_pton(af_inet, serverip.c_str(), &server.sin_addr); // 1. 字符串ip->4字节ip 2. 网络序列
n = connect(sockfd, conv(&server), sizeof(server)); // 自动进行bind哦!
if (n < 0)
{
cerr << "connect error" << endl;
ret = false;
goto end;
}
*cnt = 0;
m = read(sockfd, service_list, sizeof(service_list) - 1);
if (m > 0)
{
service_list[m] = 0;
cout << "服务器提供的服务列表是: " << service_list << endl;
}
// 并没有向server一样,产生新的sockfd.未来我们就用connect成功的sockfd进行通信即可.
cout << "请你选择服务# ";
getline(cin, inbuffer);
write(sockfd, inbuffer.c_str(), inbuffer.size());
cout << "enter> ";
getline(cin, inbuffer);
if (inbuffer == "quit")
return true;
// std::cout << "echo : " << inbuffer << std::endl;
n = write(sockfd, inbuffer.c_str(), inbuffer.size());
if (n > 0)
{
char buffer[1024];
m = read(sockfd, buffer, sizeof(buffer) - 1);
if (m > 0)
{
buffer[m] = 0;
cout << buffer << endl;
}
else if (m == 0)
{
return true;
}
else
{
ret = false;
goto end;
}
}
else
{
std::cout << "hello write error" << std::endl;
ret = false;
goto end;
}
end:
close(sockfd);
return ret;
}
// ./tcp_client serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = stoi(argv[2]);
signal(sigpipe, sig_ign);
// for (int i = 1; i <= 31; i++)
// {
// signal(i, handler);
// }
int cnt = 1;
while (cnt <= retry_count)
{
bool result = visitserver(serverip, serverport, &cnt);
if (result)
{
break;
}
else
{
sleep(1);
std::cout << "server offline, retrying..., count : " << cnt << std::endl;
cnt++;
}
}
if (cnt >= retry_count)
{
std:
cout << "server offline" << std::endl;
}
return 0;
}
发表评论