文章目录
如何在Linux中查看自己的MAC地址?
ifconfig
命令可以查看当前主机所对应的网卡信息。
我们用到的大部分局域网都是以太网标准,其中ether
对应就有”以太“的意思,而ether
后面的这个地址就是当前云服务器所对应的MAC
地址。但实际云服务器上的MAC
地址可能不是真正的MAC
地址,该MAC
地址可能模拟出来的。
网络字节序与主机字节序之间的转换
TCP/IP协议规定,网络数据流采用大端字节序,即低地址高字节。无论是大端机还是小端机,都必须按照TCP/IP协议规定的网络字节序来发送和接收数据。
发送端将发送缓冲区中的数据按内存地址从低到高的顺序发出后,接收端从网络中获取数据依次保存在接收缓冲区时,也是按内存地址从低到高的顺序保存的。
系统提供的转换函数:
#include <arpa/inet.h>
//主机数据发送到网络时,保证数据是以大端的方式发送。
//两种情况:1、如果是大端机,无需转换直接返回
// 2、如果是小端机,需要将参数转换为大端
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
//下面两个函数:
//参数是从网络中来的,两种情况:
//1、如果接收端是小端机,需要将参数装换为小端
//2、如果接收端是大端机,无需转换直接返回。
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
注意:
uint32_t
,uint16_t
分别为无符号32位整数,无符号16位整数。
socket常见API
- 创建套接字:(用于TCP/UDP协议,用于客户端/服务器)
int socket(int domain/*协议ip*/, int type/*传输方式*/, int protocol);
- 绑定端口号:(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 监听套接字:(TCP,服务器)
int listen(int sockfd, int backlog);
- 接收请求:(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 建立连接:(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
将字符串IP转换成整数IP的函数叫做inet_addr
in_addr_t inet_addr(const char *cp);
inet_addr_t:32位无符号整型
将整数IP转换成字符串IP的函数叫做inet_ntoa
char *inet_ntoa(struct in_addr in);
udp套接字通信
server端
#pragma once
#include <iostream>
#include <sys/socket.h> // strcut sockadd
#include <netinet/in.h> //struct sockadd_in
#include <arpa/inet.h> //struct sockadd_in
#include <unistd.h>
#include <string.h>
namespace net_com
{
class server
{
public:
// 创建套接字,socket()创建通信端点并返回描述符。
// int socket(int domain, int type, int protocol);
bool InitSocket()
{
int id = socket(AF_INET, SOCK_DGRAM, 0);
if (id < 0)
{
perror("socket failed");
return false;
}
std::cout << "socket success"
<< "sockid=" << id << std::endl;
sockid_ = id;
return true;
}
server(int port, std::string ip)
: port_(port), ip_(ip)
{
}
~server()
{
if (sockid_ >= 0)
close(sockid_);
}
// 绑定套接字
// int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
bool BindSocket()
{
sockaddr_in addr;
memset(&addr, '\0', sizeof(addr));
addr.sin_family = AF_INET; //协议家族
// addr.sin_addr.s_addr = inet_addr(ip_.c_str()); //字符串ip地址转换为32位无符号整数
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port_); // 正确的转换成本端机的默认存储方式
if (bind(sockid_, (sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("bind erro");
return false;
}
std::cout << "bind success" << std::endl;
sleep(2);
return true;
}
void Start()
{
#define BUFSIZE 1024
sockaddr_in access_local;
// int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
bool quit = false;
while (!quit)
{
//提供服务
// ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);
socklen_t len=sizeof(access_local);
char buf[BUFSIZE];
int size = recvfrom(sockid_, (void *)buf, BUFSIZE-1,0, (sockaddr *)&access_local, (socklen_t *)&len);
if(size==0)
{
continue;
}
buf[size]='\0';
std::cout<<"client say#"<< buf<<" "<< "access_local:"<<inet_ntoa(access_local.sin_addr)<<" localport:"<<ntohs(access_local.sin_port)<<std::endl;
sleep(1);
}
}
private:
int sockid_;
int port_;
std::string ip_;
};
}
clience端
#pragma once
#include <iostream>
#include <sys/socket.h> // strcut sockadd
#include <netinet/in.h> //struct sockadd_in
#include <arpa/inet.h> //struct sockadd_in
#include <unistd.h>
#include <string.h>
namespace net_com
{
class clience
{
public:
clience(std::string ip,int port)
: port_(port), ip_(ip), sockid_(-1)
{
}
// // 创建套接字,socket()创建通信端点并返回描述符。
// int socket(int domain, int type, int protocol);
bool InitSocket()
{
int id = socket(AF_INET, SOCK_DGRAM, 0);
if (id < 0)
{
perror("socket failed");
return false;
}
std::cout << "socket success"
<< "sockid=" << id << std::endl;
sockid_ = id;
return true;
}
// 不需要绑定,默认会绑定
void Start()
{
#define BUFSIZE 1024
// int accept(int socket, struct sockaddr *restrict address,socklen_t *restrict address_len);
bool quit = false;
sockaddr_in addr;
memset(&addr, '\0', sizeof(addr));
addr.sin_family = AF_INET; //协议家族
// addr.sin_addr.s_addr = inet_addr(ip_.c_str()); //字符串ip地址转换为32位无符号整数
addr.sin_addr.s_addr = inet_addr(ip_.c_str());//inet_addr完成了字符串和字节序的转换功能。
addr.sin_port = htons(port_);
while (!quit)
{
//提供服务
// ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);
std::string buf;
getline(std::cin, buf);
sendto(sockid_, buf.c_str(), buf.size(), 0, (struct sockaddr *)&addr, sizeof(addr));
std::cout << "client send#" << buf << std::endl;
}
}
private:
int sockid_;
int port_; // 目的端口
std::string ip_; // 目的IP
};
}
- 两份代码很像,创建套接字,显示/不显示绑定、
- 绑定成功后就可以发送或者接收数据,不需要管对方是否举报接受条件
- 所以udp的套接字用起来特别简单
INADDR_ANY
INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。- 只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡(一个ip绑定一个网卡)过来的,只要是绑定的端口号过来的数据,都可以接收到。
addr.sin_addr.s_addr = inet_addr(ip_.c_str())
inet_addr完成了字符串和字节序的转换功能。- 填入sockaddr_in 信息,目的就是可以找到建立连接,能找到对应服务。ip+port :网络文件指针
局域网种类:
- 以太网的原理:在局域网里发出的数据,在局域网内的主机都能被收到,当两台主机同时发送时会产生数据碰撞,其后碰撞主机之间都要执行碰撞避免算法和碰撞检查,在主机看来,连接的网线就是临界资源,执行碰撞避免算法本质就是互斥,上述由链路层解决。
- 令牌环局域网:只有持有令牌的主机才能在局域网内发生资源。类似互斥锁。
mac地址
用来标识全球唯一性的网卡,基本用于在局域网中标识一台唯一的主机。
路由器
路由器位于网络层及一下层,路由器能连接多个不同类型的局域网进行路由转发,路由器(ip协议)屏蔽了网络层及以下层的差异。
tcp套接字通信
server端
#pragma once
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
namespace net_com
{
class server
{
public:
server(int port)
: port_(port)
{
}
~server()
{
}
void ServiceIO(int sockid)
{
for (;;)
{
#define BUFNUME 1024
char buf[BUFNUME];
int size = read(sockid, buf, sizeof(buf) - 1);
buf[size] = '\0';
if (size > 0)
{
std::string str = buf;
str += "is received";
std::cout << str << std::endl;
write(sockid, str.c_str(), str.size());
}
else if (size == 0)
{
std::cout << "clience quit" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
int Initserver()
{
listen_sockid = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockid < 0)
{
perror("socket():");
return 1;
}
sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port_);
if (bind(listen_sockid, (sockaddr *)&local, sizeof(local)) < 0)
{
return 2;
}
if (listen(listen_sockid, 5) < 0)
{
perror("listen():");
return 3;
}
std::cout << "bind listen success socketid=" << listen_sockid << std::endl;
//服务器不断的去获取链接,获取成功后给服务器是被链接的,需要给链接客户端提供服务。
bool quit = false;
while (!quit)
{
sockaddr_in addr;
// socklen_t len ;(坑!)
// accept第三个参数:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
socklen_t len = (socklen_t)sizeof(addr);
int sockid = accept(listen_sockid, (sockaddr *)&addr, (socklen_t *)&len);
if (sockid < 0)
{
continue;
}
else
{
// 给链接提供服务,这里可以使用多线程,或者线程池,提高并发量
std::cout << "get link id:" << sockid << std::endl;
/
// version 1:单执行流服务
ServiceIO(sockid);
// version 2:多进程服务
ServiceIO(sockid);
// version 3:多线程服务
ServiceIO(sockid);
/
}
}
}
private:
int listen_sockid;
int port_;
};
}
- 因为tcp是面向连接的, a.在通信前,需要建连接 b. 然后才能通信, 一定有人主动建立(客户端,需要服务),一定有人被动接受连接(服务器,提供服务),我们当前写的是一个server, 周而复始的不间断的等待客户到来,我们要不断的给用户提供一个建立连接的功能, 设置套接字是Listen状态, 本质是允许用户连接
- 再同一时间里可能存在大量的服务端请求链接,服务端可能处理不来,listen设置的状态会将链接管理起来,其实就是管理内核描述链接的结构体。
- 客户端发送数据,服务端处理数据并把结果给回客户端,服务端与客户端通信,本质服务端给客户端提供IO服务。
- 当服务端获取到链接时,就可以进行tcp通信了,这里服务端可以提供两种类型的服务,a、单执行流服务,b、多进行服务(多进程,多线程)。
version 1:单执行流服务
注意:单进程版,没人使用。
else
{
// 给链接提供服务,这里可以使用多线程,或者线程池,提高并发量
std::cout << "get link id:" << sockid << std::endl;
ServiceIO(sockid);
}
version 2:多进程服务
方法一:
else
{
// 给链接提供服务,这里可以使用多线程,或者线程池,提高并发量
std::cout << "get link id:" << sockid << std::endl;
//方法一:
signal(SIGCHLD, SIG_IGN); // 忽略子进程退出
//子进程
if (fork() > 0) // 父进程进程
{
continue;
}
//子进程为孤儿进程,由OS托管
ServiceIO(sockid);
}
方法二:
else
{
// 给链接提供服务,这里可以使用多线程,或者线程池,提高并发量
std::cout << "get link id:" << sockid << std::endl;
// 方法二、
int id = fork();
if (id == 0)
{
close(listen_sockid); //这里爷爷进程才需要的fd,所以父进程关闭,孙子进程也就没继承了,关闭不属于该进程的fd提高安全性。
if (fork() > 0)
exit(0); //父进程创建孙子进程后立马退出
//孙子进程为孤儿进程,由OS托管
ServiceIO(sockid);
close(sockid);
exit(0);
}
else
{
//爷爷进程等待父进程,父进程创建孙子进程后立马退出,只是短时间的阻塞不会造成很大的影响
waitpid(id, nullptr, 0);
close(sockid);
}
}
version 3:多线程服务
else
{
// 给链接提供服务,这里可以使用多线程,或者线程池,提高并发量
std::cout << "get link id:" << sockid << std::endl;
//
// 方法三、多线程方式
using namespace ns_pthread_pool;
// 解析数据,然后插入到任务队列,线程池里的线程自动会拿取任务执行。
// sockid为链接套接字,提供网络需要用到它,所以我们设计一个Task,里面就是一个套接字的成员变量,线程获取该任务,通过该变量就能网络通信。
// 1.构建一个任务
// 2.将任务放到任务队列里
Tack t(sockid);
pthread_pool<Tack>::GetInstance()->TaskPush(t);
}
Tack类:
#pragma once
#include <stdio.h>
#include <pthread.h>
#include <string>
class Tack
{
private:
int sockid;
public:
Tack(int id)
: sockid(id)
{
}
void run()
{
//for 死循环为长服务,这里我们使用短服务
// for (;;)
// {
#define BUFNUME 1024
char buf[BUFNUME];
int size = read(sockid, buf, sizeof(buf) - 1);
buf[size] = '\0';
if (size > 0)
{
std::string str = buf;
str += "is received";
std::cout << str << std::endl;
write(sockid, str.c_str(), str.size());
}
else if (size == 0)
{
std::cout << "clience quit" << std::endl;
// break;
}
else
{
std::cerr << "read error" << std::endl;
// break;
}
// }
// 执行完以后我们还要关闭链接,所以此时的为短链接。
close(sockid);
}
// 仿函数
void operator()()
{
return run();
}
};
线程池:
#pragma once
#include <pthread.h>
#include <iostream>
#include <queue>
// 总结:我们应该清楚生产者消费者模型的目的是为了让生产和消费解耦,解耦的好处在于,生产和消费能并发执行,
// 在多生产者多消费者模型中,目的就为了能合理控制生产消费线程的个数达到合理利用资源。
namespace ns_pthread_pool
{
const int g_default_val = 3;
template <class T>
class pthread_pool
{
private:
int num_; //线程数量
std::queue<T> task_queue_; //任务队列,供给线程池使用
pthread_cond_t task_queue_cond_; // 任务队列的条件变量
//拿取数据的过程要消费者间互斥,消费者与生产者互斥同步,所以我们需要添加锁来进行保护,并且加入条件变同步
pthread_mutex_t mtx_; // 多生产者多消费者维护关系的共有锁资源,
// 单例模式使用情况:1、该类对象只能被创建一个对象2、该类存储的数据加载的内存很大。
// 单例模式使用目的:
// 单例模式如何使用:
// 两种实现方式:
// 懒汉方式(常用):
// 1、添加该类成员静态变量2、提供静态获取实例的方法
// 3、构造函数为私有(在类外调用不到,通过静态获取实例的方法控制该类只能创建一个对象)
// 4、构造函数由静态获取实例的方法调用
static pthread_pool<T> *ins_;
//私有构造函数
pthread_pool(int num = g_default_val /*线程数量*/)
: num_(num)
{
pthread_cond_init(&task_queue_cond_, nullptr);
pthread_mutex_init(&mtx_, nullptr);
}
private:
// 对成员变量的访问目的为了让静态函数通过接收this参数访问成员函数并且访问成员变量。
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void UnLock()
{
pthread_mutex_unlock(&mtx_);
}
bool IsEmpty()
{
return task_queue_.empty();
}
void WakeUpThread()
{
pthread_cond_signal(&task_queue_cond_);
}
public:
static pthread_pool<T> *GetInstance()
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 当前单例对象还没有被创建
if (ins_ == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
{
pthread_mutex_lock(&lock);
// ins为空说明该类没有创建过对象
if (ins_ == nullptr)
{
ins_ = new pthread_pool<T>();
ins_->PthreadPoolInit();
std::cout << "该类第一次创建对象" << std::endl;
}
}
return ins_;
}
//析构函数
~pthread_pool()
{
pthread_cond_destroy(&task_queue_cond_);
pthread_mutex_destroy(&mtx_);
}
void TaskPop(T &out)
{
//拿取数据的过程要消费者间互斥,消费者与生产者互斥同步,所以我们需要添加锁来进行保护,并且加入条件变同步
Lock();
//线程在满拿取条件时拿取数据处理,否则等待条件变量
while (IsEmpty())
{
// 线程池的线程充当消费者
pthread_cond_wait(&task_queue_cond_, &mtx_);
}
out = task_queue_.front();
task_queue_.pop();
UnLock();
}
void TaskPush(T &in)
{
Lock();
task_queue_.push(in);
UnLock();
WakeUpThread();
}
// 线程池线程
//我们必须设置成静态的函数,原因是成员函数有this参数
static void *routine(void *agrs)
{
//
pthread_pool<T> *pp = (pthread_pool<T> *)agrs;
while (true)
{
T task(-1);
pp->TaskPop(task);
task.run();
}
}
//初始化线程池
void PthreadPoolInit()
{
pthread_t id;
for (int i = 0; i < num_; i++)
{
pthread_create(&id, nullptr, routine, this /*线程池对象this指针*/);
}
}
};
template <class T>
pthread_pool<T> *pthread_pool<T>::ins_ = nullptr;
}
socketAPI接口的本质
1.创建socket的过程,socket(),本质是打开文件,仅仅有系统相关的内容,不包含网络的内容。
2.bind(),struct sockaddr_in ->ip,port,本质是ip+port 和文件信息进行关联
3.listen(),本质是设置该socket文件的状态,允许别人来连接我。
4.accept(), 获取新链接到应用层,是以fd为代表的。这里的连接本质就一个结构体。
5.read/write,本质就是进行网络通信,但是对于用户来讲,相当于我们在进行文件读写。
6.close(fd),关闭文件,a.系统层面释放曾经申请的文件资源,连接资源等。b.网络层面,通知对方,我的连接已经关闭了。
7.connect(),本质数发起链接,在系统层面,就是构建一个请求报文发送过去,这里的报文数由传输层进行封装的,具体细节在学习每层协议时具体学习,在网络层面,发起tcp链接的三次握手。
8.close(),clience和server,本质在网络层面,其实就数进行四次挥手(具体细节会在传输层协议学习)。
编写一个计算器的应用层网络服务(tcp,自定义协议)
CS模式的在线版本计算机,本质就是一个应用层网络服务,我们主要完成下三点即可:
- 基本通信代码 (OSI里的会话层)
- 序列化和反序列化(OSI里的表示层)(使用组件JSON完成)(本质就是自定义应用层协议)
- 业务逻辑(OSI里的应用层)(计算器的实现)
通过下面的实现网络服务,我们会发现实现起来非常麻烦,所有后续我们学习大佬设计的HTTP,HTTPS应用层协议,把OSI会话层,表示层和应用层,合并成HTTP、HTTP等协议。
说明:
为什么要序列化和反序列化?
一般情况下,我们把数据结构化。其实客户端发送请求时,客户端的请求是一种结构体,那么服务端也需要通过请求结构体进行接收,这种方式我们称为类型序列化,但是这种方式有缺陷的,例如:当服务端进行升级时,请求的结构体添加了某个字段,旧的客户端没有升级代码,这时两端的请求结构体都不一样,那么类型序列化将没有用了。
所以我们一般不使用类型序列化,而是使用第三组件 JSON工具帮我们实现序列化和反序列化的过程,JSON组件使我们不会出现类型序列化的问题了。
安装json(序列化反序列化工具)
安装:
sudo yum install -y jsoncpp-devel
头文件路径:
/usr/include/jsoncpp/json/
用法实例:JSON::Value 类可以承装任何对象, json是一种kv式的序列化方案
#include<jsoncpp/json/json.h>
#include"Protocol.hpp"
int main()
{
// 结构体 转换为 字符串
request_t req={10,20,'+'};
Json::Value val;
val["x"]=req.x;
val["y"]=req.y;
val["op"]=req.op;
Json::FastWriter w;
std::string s= w.write(val);
std::cout<<"序列化\n";
std::cout<<s<<std::endl;
// 字符串 转换为 结构体
std::cout<<"反序列化\n";
request_t req2;
Json::Reader r;
Json::Value value;
r.parse(s,value);
req2.op=value["op"].asInt();
req2.x=value["x"].asInt();
req2.y=value["y"].asInt();
std::cout<<req2.to_string();
return 0;
}
运行结果:
1.定义应用层协议
Protocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
using namespace std;
// 定制协议的过程,目前就是定制结构化数据的过程
// 请求格式
// 我们自己定义的协议,client && server 都必须遵守! 这就叫做自定义协议
typedef struct request
{
int x; // 10
int y; // 0
char op; // '/' "+-*/%"
std::string to_string()
{
std::string s;
s += std::to_string(x);
s += op;
s += std::to_string(y);
s += '\n';
return s;
}
} request_t; // 10/0
// 响应格式
typedef struct response
{
int code; // server运算完毕的计算状态: code(0:success), code(-1: div 0) ...
int result; // 计算结果, 能否区分是正常的计算结果,还是异常的退出结果
std::string to_string()
{
std::string s;
s+=std::to_string(code);
s+=" ";
s+=std::to_string(result);
s+="\n";
return s;
}
} response_t;
//序列化需求
std::string SerializeRequest(request_t &req)
{
Json::Value val;
val["x"] = req.x;
val["y"] = req.y;
val["op"] = req.op;
Json::FastWriter w;
std::string s = w.write(val);
return s;
}
//反序列化需求
void DeserializationRequest(std::string s,request_t& req)
{
Json::Reader r;
Json::Value value;
r.parse(s,value);
req.op=value["op"].asInt();
req.x=value["x"].asInt();
req.y=value["y"].asInt();
}
//序列化响应
std::string SerializeResponse(response_t &rep)
{
Json::Value val;
val["code"]=rep.code;
val["result"]=rep.result;
Json::FastWriter w;
std::string s = w.write(val);
return s;
}
//反序列化响应
void DeserializationResponse(std::string s,response_t&rep)
{
Json::Reader r;
Json::Value value;
r.parse(s,value);
rep.code=value["code"].asInt();
rep.result=value["result"].asInt();
}
2.完成网络通信和业务逻辑处理
封装网络套接字接口 Soch.hpp
#pragma once
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <iostream>
class Sock
{
public:
static int Socket()
{
int sockid = socket(AF_INET, SOCK_STREAM, 0);
if (sockid < 0)
{
perror("socket:");
exit(2);
}
return sockid;
}
static void Bind(int sockid, int port)
{
struct sockaddr_in addr;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
if (bind(sockid, (const sockaddr *)&addr, sizeof(addr)) < 0)
{
std::cerr << "bind error" << std::endl;
exit(3);
}
}
static void Listen(int sockid)
{
if (listen(sockid, 5) < 0)
{
std::cerr << "listen erro" << std::endl;
exit(4);
}
}
static int Accpet(int listen_sockid)
{
sockaddr_in addr;
socklen_t len = sizeof(addr);
int fd = accept(listen_sockid, (sockaddr *)&addr, &len);
if (fd >= 0)
return fd;
return -1;
}
static void Connect(int sockid, std::string ip, u_int16_t port)
{
sockaddr_in local;
local.sin_addr.s_addr = inet_addr(ip.c_str());
local.sin_family = AF_INET;
local.sin_port = htons(port);
if (connect(sockid, (const sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "connect error" << std::endl;
exit(5);
}
}
};
服务端代码 Server.cc
#include "Sock.hpp"
#include "Protocol.hpp"
#include <pthread.h>
void *routine(void *argv)
{
int linkid = *(int *)argv;
delete (int *)argv;
pthread_detach(pthread_self());
// 业务逻辑这里为 短服务
// for (;;)
// {
//提供服务了;
// version 1 : 利用类型进行序列化和反序列化,不实用
// request_t req;
// int size=read(linkid,(void*)&req,sizeof(req));
// if(size==sizeof(req))
// {
// // 读取到完整的数据
// int a=req.x;
// int b=req.y;
// char op=req.op;
// // 简单的返回加法结果,不做-/*等运算方法了;
// response_t rep;
// rep.code=1;
// rep.result=a+b;
// write(linkid,(void*)&rep,sizeof(rep));
// }
// version 2 : 利用Json工具进行序列化和反序列化
request_t req;
#define BUFNUME 1024
char buf[BUFNUME];
int size = read(linkid, (void *)buf, sizeof(buf) - 1);
buf[size] = 0;
std::string s = buf;
DeserializationRequest(s, req);
response_t resp;
// 简单的返回加法结果,不做-/*等运算方法了;
resp.code = 1;
resp.result = req.x + req.y;
std::string str = SerializeResponse(resp);
write(linkid, (void *)str.c_str(), str.size());
std::cout << "服务结束\n";
close(linkid);
// }
}
void Upage(const char *s)
{
std::cout << s << " port" << std::endl;
exit(1);
}
// Server port
int main(int argc, char const *argv[])
{
if (argc != 2)
{
Upage(argv[0]);
}
uint16_t port = atoi(argv[1]);
int listen_sockid = Sock::Socket();
Sock::Bind(listen_sockid, port);
Sock::Listen(listen_sockid);
//
std::cout << "create success\n ";
for (;;)
{
int link_id = Sock::Accpet(listen_sockid);
if (link_id < 0)
{
continue;
}
// 创建线程,由线程处理链接套接字网络服务,所以线程只需要获取链接套接字即可完成网络服务
pthread_t pthread_id;
int *id_p = new int(link_id);
pthread_create(&pthread_id, nullptr, routine, (void *)id_p);
}
return 0;
}
客户端代码 Clience.cc
#include "Sock.hpp"
#include "Protocol.hpp"
void Upage(const char *s)
{
std::cout << s << "ip port" << std::endl;
exit(1);
}
int main(int argc, char const *argv[])
{
if (argc != 3)
{
Upage(argv[0]);
}
u_int16_t port = atoi(argv[2]);
std::string ip = argv[1];
int sockid = Sock::Socket();
Sock::Connect(sockid, ip, port);
// 发送请求.........
request_t req;
std::cout << "请输入 one\n";
std::cin >> req.x;
std::cout << "请输入 two\n";
std::cin >> req.y;
std::cout << "请输入 operator\n";
std::cin >> req.op;
std::string s = SerializeRequest(req);
write(sockid, (void *)s.c_str(), s.size());
// 阻塞等待服务器响应
response_t rep;
#define BUFNUM 1024
char buf[BUFNUM];
int size = read(sockid, (void *)buf, sizeof(buf));
buf[size] = 0;
std::string str = buf;
DeserializationResponse(str, rep);
if (rep.code == 1)
{
std::cout << "response success result:" << rep.result << std::endl;
}
else
{
std::cout << "response failed" << std::endl;
}
return 0;
}
makefile 编译上述所有文件
.PHONY:all
all:Clience Server
Clience:CalClience.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
Server:CalServer.cc
g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
.PHONY:clean
clean:
rm -rf Clience Server
运行结果:
HTTP协议
HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示browser。
由于HTTP是基于请求和响应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这就是学习HTTP的重点。
HTTP请求协议格式
HTTP请求协议格式如下:
HTTP请求由以下四部分组成:
- 请求行:[请求方法]+[url]+[http版本]
- 请求报头:请求的属性,这些属性都是以key: value的形式按行陈列的。
- 空行:遇到空行表示请求报头结束。
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。
如何将HTTP请求的报头与有效载荷进行分离?
在应用层一下协议完成了通信细节,HTTP协议是需要序列化和反序列的。其有利于网络通信。无论是请求还是响应,基本上http都是按照行(\n)为单位构建请求或者响应的。结构体序列化与其不同,其是按\n分割组成的大字符串也是一种序列化的形式。
1、我们可以根据HTTP请求当中的空行来进行分离,当服务器收到一个HTTP请求后,就可以按行进行读取,如果读取到空行则说明已经将报头读取完毕,实际HTTP请求当中的空行就是用来分离报头和有效载荷的。如果将HTTP请求想象成一个大的线性结构,此时每行的内容都是用\n隔开的,因此在读取过程中,如果连续读取到了两个\n,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。
2、通过Content-Length的报文属性,可以确认有效载荷的具体长度,确认获取到完整的数据。
cookie
cookie是用来保存用户私密信息的文件。目的提高用户访问网站或平台体验,原因http是无状态。
文件类型:
- 内存级别(浏览器内存,看不到)
- 文件级别(磁盘文件)
状态码浏览器不做处理,服务端将请求进行文本分析,然后得到对应的响应内容(包含客户端当前状态码,响应正文内容)。
https 加密
对称加密:只有密钥
非对称加密:只有私钥,公钥
两者都是不安全的。
首先,第三方有公钥和私钥,私钥用来加密公钥,域名等信息形成数字签名。公钥等信息和数字签名合并成为证书。
证书能够保证公钥等信息不被修改。因此,我们首先进行非对称加密,拿取对方的密钥,此时密钥是被加密过的,并且是由我方的公钥进行加密,因此该密钥只有双方能知道,后面数据传送我们只需要进行对称加密即可。
tcp
服务端状态转化:
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态转化:
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入
FIN_WAIT_1; - [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.
连接管理机制
为什么要三次握手?
a.验证双方网络,io状态等是否就绪。
b.验证全双工,三次握手,是能看到双方都有收发的能力的 最小的次数。
为什么是三次握手?
三次握手可行的原因关键在于,最后一次握手(ACK),双方达成一个“没有消息就是最好的消息”的共识,即如果服务端收到ACK相应那么说明客户端已经收到连接报文,并且如果客户端在发送完ACK后,没有收到服务端的RST标志的报文,那么说明服务端已经收到了三次握手的最后一次报文了。
其余解析:
a.一次或者两次握手验证不了a和b两点,如果是超过3次握手,那么3次握手之后的都是多余的,这样会增加链接成本。
b.如果是一次两次握手,那么将会遭到“SYN洪水攻击”,使服务器要维护大量的“链接资源”.
c.如果是三次握手那么也会受到“SYN洪水攻击”,只是双方都要付出等价的代价,因此这种手段只能限制小数量服务器的攻击,如果有大量的肉机被安装木马病毒,大量的攻击,那么服务端也会垮掉。
d.其实“洪水攻击”是有防御手段的,通过IP地址……
为什么要“四次挥手”
断开链接本质:双方达成断开链接的共识,其实就是一个通知对方的机制。
四次挥手是协商断开链接的最小次数。
四次挥手的状态
主动断开链接的一方,要进入一个TIME_WAIT状态,进入该状态说明主动方“四次挥手”已经完成了。进入TIME_WAIT状态确保四次挥手的最后一次ACK能被接收到。
四次挥手时的状态变化如下:
- 在挥手前客户端和服务器都处于连接建立后的ESTABLISHED状态。
- 客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为FIN_WAIT_1。
- 服务器收到客户端发来的连接断开请求后对其进行响应,此时服务器的状态变为CLOSE_WAIT。
- 当服务器没有数据需要发送给客户端的时,服务器会向客户端发起断开连接请求,等待最后一个ACK到来,此时服务器的状态变为LASE_ACK。
- 客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态。
- 当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为CLOSED状态。
- 而客户端则会等待一个2MSL(Maximum Segment Lifetime,报文最大生存时间)才会进入CLOSED状态。
四次挥手中前三次挥手丢包时的解决方法:
- 第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传。
- 第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
- 第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传。
理解TIME_WAIT状态
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是:
这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口.我们用netstat
命令查看一下:
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
- 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
- MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout
查看msl的值;
想一想, 为什么是TIME_WAIT的时间是2MSL?
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话
- 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文(ACK)可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
解决TIME_WAIT状态引起的bind失败的方法
bind失败是因为,服务端主动退出而导致大量的链接进入TIME_WAIT状态,TIME_WAIT状态的目的是让遗留数据消散和保证最后一个ACK被对方收到,所以在服务端再次启动bind定时,协议规定必须等待所有的绑定该端口的TIME_WAIT连接消散后,才能再次bind。
因此有些场景下是不合理的,例如:
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
- 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip, 目的端口, 协议).
- 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现bind失败的问题。
解决方法:
使用setsockopt()
设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
setsockopt()
设置以后解决了不能绑定相同端口的新套接字,后序可以通过序号(或者时间戳)来区分接收数据是属于新连接还是旧链接的,旧连接发送的数据肯定早于新连接被创建的时间,区分的目的是因为旧连接与新连接的五元组相同,当报文传输到传输层后,根据端口号和序号的判断下交付给特定的连接。因此,新连接只会接收到新的数据,老旧连接的数据不会发送到新的连接上。
理解 CLOSE_WAIT 状态
server不关闭客户端连接(close(linkfd)
),这会导致服务端导致一个TIME_WAIT
状态的链接。这会导致一个无用的连接长时间停留在系统里,如果存在大量的CLOSET_WAIT
状态的连接那么系统资源会越来越少,导致fd泄漏。
我们编译运行服务器. 启动客户端链接, 查看 TCP 状态, 客户端服务器都ESTABLELISHED
状态, 没有问题.然后我们关闭客户端程序, 观察 TCP 状态
tcp 0 0 0.0.0.0:9090 0.0.0.0:* LISTEN 5038/./dict_server
tcp 0 0 127.0.0.1:49958 127.0.0.1:9090 FIN_WAIT2 -
tcp 0 0 127.0.0.1:9090 127.0.0.1:49958 CLOSE_WAIT 5038/./dict_server
此时服务器进入了 CLOSE_WAIT 状态, 结合我们四次挥手的流程图, 可以认为四次挥手没有正确完成.
小结: 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题.
滑动窗口
刚才我们讨论了确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是4000个字节(四个段).
- 发送前四个段的时候, 不需要等待任何ACK, 直接发送;
- 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
- 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
- 窗口越大, 则网络的吞吐率就越高;
理解滑动窗口
- 如上图发送端调用write()实际就是把数据拷贝到传输层的发送缓冲区里,数据什么时候发送由传输层决定。
- 发送缓冲区里1号区为已经发送,已经确认的数据;2号区为可以/已经发送,但是还没收到确认;3号区为没有发送的数据。
2号区就是滑动窗口,我们来分析滑动窗口的一些细节问题。
如上图,收到2001序号的确认应答,我们的滑动窗口往右移动一个分段。
窗口的大小与接收方的接收能力强相关
窗口的大小不会一直不变,根据接收方的接收能力去调整。
丢包重传
情况一:数据包已经抵达, ACK被丢了
- 这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;
- 接收方是根据序号顺序发送确认应答的,比如1,3,4序号的数据都被接收,但是2号序号的数据还没被接收,接收方只会发送1号序号的确认应答,因为双方都达成了协议,确认序号代表确认序号前的数据都被接收了,这时候如果发送确认序号4,那么接收方就认为2号序号被接收了,后续2号序号数据丢失将不会被重传。
情况二: 数据包就直接丢了
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中
快重传VS超时重传
超时重传前提条件是等待2MSl时间后没有收到应答才会执行重传。
快重传是收到3次相同的应答后才会重传。
区别:
- 快重传效率比超时重传效率高,原因是超时需要等待2MSL。
为什么不全使用快重传?
原因是快重传要有前提条件的,如果满足不了收到3次相同的应答,那么快重传不能使用。所以说在网络层快重传与超时重传都会使用到。