在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
两台主机互相通信,一定是一台主机把数据交给另一台主机,所以发送数据的主机,它发出去的报文里一定会携带自己的IP和对方的IP。就好比唐僧,走到哪儿都要告诉别人,都能告诉别人我从哪来到哪去。那么一个报文一定要携带原IP和目的IP的。原IP是为了方便别人把消息再回给你,那么目的IP是为了那么方便我们在路上根据IP地址进行路由,并且确认我们最终的报文已经到达对方主机。
网络通信的本质:其实是进程间通信
端口号(port)是传输层协议的内容.
uint16_t
一个进程可以绑定多个端口号(比如你有手机号,身份证号)但是一个端口号不可以被多个进程绑定(比如你的身份证号可以代表很多人吗)。
IP地址 + 端口号(Port) = 互联网中的唯一个进程(目前这样理解)
是通过IP+PORT构建进程唯-一性,来进行的基于网路的进程问通信!
(源IP,源端口) + (目标IP,目标端口号) = socket通信
端口号可以是进程PID吗,进程PID也不是唯一的吗?
如果用进程PID能实现这个功能的。但谁是更好的呢?如果你用了PID,那么这就有问题了。你用TD标识网络标识进程的通信绝对没问题,但是如果你用PID来做了,那么这里就有这样的问题。不是所有的进程都要进行网络通信。那么只有部分进程可能会网络通信。我们就很难区分清楚哪些进程是进行网络通信的,哪些不是。你要区分你就得加字段。你都加字段了,为什么不用端口呢。这是其一,当然,这不是最重要的。最重要的是。如果你用PID来作为,那么网络当中标识该进程。那么你换句话说呢,你在网络,就是网络功能的设计上,你必须得使用PID这样的字法。而PID是一个操作系统层面进程管理的概念。也就意味着你的网络模块儿也要包含进程管理的部分,要不然你无法认识端口。所以这样的话呢,就增加了系统当中进程管理和网络管理的耦合度。那么在设计字段的时候呢?你既得考虑PID,那么同学们,未来你改一下PID,你是不还可能影响网络?所以我们的设计者就认为没必要。我们设计单独端口号,你是你的,我是我的,我们不要燃。那么这样我们就能在逻辑上实现很好的解耦,这才是最重要的理由。
TCP(Transmission Control Protocol 传输控制协议)
UDP(User Datagram Protocol 用户数据报协议)
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
云服务器,不需要bind ip地址,需要让服务器自己指定IP地址,自己本地装的虚拟机,或者物理机器是允许的bind ip地址,为什么?
云服务器上不是,只有一张网卡,它可能有多张网卡?也可能多张网卡当中配有多个IP。如果你的服务器配了两个IP,IP1和IP2,而你自己写的软件udp服务器绑定的时候只绑定了某一个IP,那也就意味着你的服务器上只能接收来自于某一个IP上面给你递交的数据报。所以呢我们就很很明显的就可能会导致你的服务器处理数据量就变少了。那么我们最想期望的就是,只要是发送到这台机器上的数据。那么我们到了就我的机器上,我们接下来要做的不应该用IP来甄别数据,而剩下的就是只要是给我特定端口的,你都转给我。
udp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
enum
{
SOCKET_ERR = 1,
BIND_ERR
};
const static uint16_t default_port = 8888;
class UdpServer
{
public:
UdpServer(uint16_t port = default_port) : port_(port)
{
std::cout << "server addr: " << port_ << std::endl;
}
void InitServer()
{
// 1. 创建socket接口,打开网络文件
sock_ = socket(AF_INET, SOCK_DGRAM, 0);
if (sock_ < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
std::cout << "create socket success: " << sock_ << std::endl; // 3
// 2. 给服务器指明IP地址(??)和Port
struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
bzero(&local, sizeof(local));//local将数据置0
local.sin_family = AF_INET; // AF_INET和PF_INET一样是宏替换
// 网络传输包文中要携带[源ip,源端口 目标ip,目标端口]
// 所以ip和端口号转化成为网络序列
local.sin_port = htons(port_); // port_端口号是主机序列,要将主机序列端口号转换成网络序列端口号
// 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 不能强制类型转换
// 2. 需要将主机序列转化成为网络序列
// local.sin_addr.s_addr = inet_addr(ip_.c_str());
// inet_addr将点分十进制的字符串转化成in_addr_t类型,同时将主机序列ip地址转化成为网络序列
// 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP
// #define INADDR_ANY ((in_addr_t) 0x00000000) 可以主机转网络但是没有意义因为是全零htonl(INADDR_ANY)
// typedef uint32_t in_addr_t
// 服务端bind将填充好套接字字段,文件字段,绑定关联成网络文件,因为这些数据是在栈上的使用bind才能绑定成网络的
if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success: " << sock_ << std::endl;
}
void Start()
{
char buffer[1024];
while (true)//云服务器是要24小时运行的所以是死循环
{
// 收
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n > 0)
buffer[n] = '\0';
else
continue;
std::cout << "recv done ..." << std::endl;
// 提取client信息 -- debug
// 结构体peer是通过网络发送过了的,所以 需要网络序列将转化成为主机序列
// 这里网络序列将转化成为主机序列的函数可以理解为,都是传值调用没有改变原值
std::string clientip = inet_ntoa(peer.sin_addr); // inet_ntoa将转化成点分十进制的字符串的char* 类型,同时将网络序列ip地址转化成为主机序列
uint16_t clientport = ntohs(peer.sin_port); // 将网络序列转化成为主机序列
//上面的操作只是读取并没有改变peer本身的内容
std::cout << clientip << "-" << clientport << "# " << buffer << std::endl;
sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, sizeof(peer));
}
}
~UdpServer() {}
private:
int sock_;
uint16_t port_;
};
udp_server.cc
#include
#include
#include
#include
#include "udp_server.hpp"
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " port\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(-1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<UdpServer> usvr(new UdpServer);
usvr->InitServer();
usvr->Start();
return 0;
}
#include
#include
#include
#include
#include
#include
#include
// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl;
}
// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(-1);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(-1);
}
// client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
// 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
// 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
// 需要统一规范化
// 明确server是谁
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while(true)
{
//多线程化??
// 用户输入
std::string message;
std::cout << "[我的服务器]# ";
// std::cin >> message;
std::getline(std::cin,message);
// 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
//发送
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//接受
char buffer[2048];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
if(n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
客户端什么时候bind的?
在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文发送
每一台机器都有一个默认ip127.0.0.1
本地环回,就表示当前主机,通常用来进行本地通信或着测试用。
问题 1
我在进行我们正常创建套接字,这里local.sin_port = htons(port_);
,我在进行创建套接字绑定的时候,设置端口和IP,以端口回为例。需要主机转网络。可是我发现为什么我在进行,收消息的时候,发消息的时候,我们并没有做任何的主机转网络的转化。难道这里就不需要做转化吗?
答案:不需要,因为这种传送数据的,那么recvfrom
和sendto
,它会自动在底层帮我们做大小端的转化。以及TCP的读写方法。也照样是进行自动为我们做大小端的转化,只有你在绑定阶段啊,在启动服务器时的,那么有一些属性是需要由用户去把它大小的转化的
我们以前在讲udp的时候,我们发现这个套阶段udp通篇只有一个套接字呀,为什么accept
的返回值还是一个套接字(文件描述符)?
我们accept它就要传入的这个参数,我们称为监听套接字,就好比我们进入一个饭店,门外有招揽客人的,饭店内有真正提供服务,监听套接字就是门外有招揽客人(只有一个)。而我们每一次一个链接到来时,我们的accept会返回一个文件描述符,那么返回值,它也的一个文件描述符,它就它的作用呢,是真正给用户提供数据IO服务的,
如果TCP没有多执行流的话,它只能处理一个客户端。那为什么UDP就可以处理多个客户端呢?
UDP它单进程能受理多客户端的原因是因为他收取数据是不需要链接。不需要accept
等待,而今天我们在读取时,我们是在进行获取新连接需要accept
等待。而当你的主执行流进程或线程在获取新链接时。你的主执行流进入service,来提供服务时,给一个人进行数据读取的候。那么在给这一个人进行数据读取的时候,那么我们对应的主执行流是不是就没有办法再去accept了?就导致我们的服务一次只能给一个客户端提供服务。
tcpServer.hpp
#pragma once
namespace tcp_server
{
using func_t = std::function<std::string(const std::string)>;
class tcpServer;
class ThreadData
{
public:
ThreadData(int fd, tcpServer* ts)
:sock(fd)
,current(ts)
{}
public:
int sock;
tcpServer *current;
};
class tcpServer
{
public:
tcpServer(func_t func, uint16_t port = 8080)
:port_(port)
,func_(func)
{}
void initServer()
{
listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
if(listenSock_ < 0)
{
std::cout<< "创建套接字失败"<< std::endl;
exit(-1);
}
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);//可以不用转
//local 数据是在栈上的使用bind才能绑定成网络的
if(bind(listenSock_, (struct sockaddr*)&local, sizeof(local)) < 0)//与c++ std::bind不一样
{
std::cout<< "绑定失败"<< std::endl;
exit(-1);
}
//监听
if(listen(listenSock_, 32) > 0)
{
std::cout<< "监听失败"<< std::endl;
exit(-1);
}
}
void start()
{
while(true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
//获取连接
int sock = accept(listenSock_, (struct sockaddr*)&client, &len);
//sock 这个描述符才是真为提供服务的
if(sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
// 提取client信息 -- debug
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
//获取连接成功,开始进行处理
// 5. 获取新连接成功, 开始进行业务处理
std::cout << "获取新连接成功: " << sock << " from " << listenSock_ << ", "<< clientip << "-" << clientport << std::endl;
pthread_t tid;
ThreadData *td = new ThreadData(sock, this);
pthread_create(&tid, nullptr, threadRoutine, td);
// v4: 一旦用户来了,你才创建线程, 线程池吗??
//使用线程池的时候,一定是有限的线程个数,一定是要处理短任务
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
ThreadData *td = static_cast<ThreadData *>(args);
td->current->service(td->sock);
delete td;
return nullptr;
}
void service(int sock)
{
char arr[1024] ={0};
while(true)
{
//linux下一切皆文件
ssize_t s =read(sock, arr, sizeof(arr) - 1);
//read返回实际读取的字节数,读取失败返回-1
if(s > 0)
{
arr[s] = '\0';//文件中不存放\0
std::string res = func_(arr);
std::cout << ">>> " << res << std::endl;
write(sock, res.c_str(), res.size());
}
else if(s == 0)
{
//管道通信时,只有对方关闭文件是s才会等于0
close(sock);
std::cout <<" quit, me too" << std::endl;
break;
}
else
{
close(sock);
break;
}
}
}
~tcpServer()
{}
private:
uint16_t port_;
int listenSock_;
func_t func_;
};
}
tcpServer.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "error.hpp"
#include "tcpServer.hpp"
#include "daemon.hpp"
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}
std::string echo(const std::string& str)
{
return str;
}
int main(int argc, char* argv[])
{
// 准备工作
if(argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = std::atoi(argv[1]);
std::unique_ptr<tcp_server::tcpServer> tcp(new tcp_server::tcpServer(echo, port));
tcp->initServer();
//将服务器守护进程化
//Daemon();
tcp->start();
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "error.hpp"
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << " serverip serverport\n"<< std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 1. create socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "socket error : " << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// client要不要bind? 要
// client要不要自己bind? 不要,因为client要让OS自动给用户进行bind
// client不需要listen(监听)?
//client不需要accept(获取连接)
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_aton(serverip.c_str(), &(server.sin_addr));
int cnt = 5;
while(connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
{
sleep(1);
std::cout << "正在给你尝试重连,重连次数还有: " << cnt-- << std::endl;
if(cnt <= 0) break;
}
if(cnt <= 0)
{
std::cerr << "连接失败..." << std::endl;
exit(CONNECT_ERR);
}
char buffer[1024];
// 3. 连接成功
while(true)
{
std::string line;
std::cout << "Enter>>> ";
std::getline(std::cin, line);
write(sock, line.c_str(), line.size());
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo >>>" << buffer << std::endl;
}
else if(s == 0)
{
std::cerr << "server quit" << std::endl;
break;
}
else
{
std::cerr << "read error: " << strerror(errno) << std::endl;
break;
}
}
close(sock);
return 0;
}
因为当前你在启动一个进程的时候,是在当前bach的会话当中,你先写一个任务,这个是种新起的任务,只有一个进程,就是你自己,那么你自己就是自称进程组组长,组成一个任务,你就是组长,
守护进程一但开启就与终端没有关系了
#pragma once
// 1. setsid();
// 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
// 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /
//守护进程的本质:是孤儿进程的一种!
void Daemon()
{
// 1. 忽略信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2. 让自己不要成为组长
if (fork() > 0)
exit(0);
// 3. 新建会话,自己成为会话的话首进程
pid_t ret = setsid();
if ((int)ret == -1)
{
std::cout<<"新建会话失败"<<std::endl;
exit(-1);
}
// 4. 可选:可以更改守护进程的工作路径
// chdir("/")
// 5. 处理后续的对于0,1,2的问题
int fd = open("/dev/null", O_RDWR);//“/dev/null”信息黑洞,或者垃圾桶,这个文件向他里面写什么也写不进去,向他里面读什么也读不到
//“/dev/null”这个文件是必须要存在的,任何linux系统都要有这个文件
if (fd < 0)
{
std::cout<<"处理0,1,2问题失败"<<std::endl;
exit(-1);
}
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}