目录
一计算机网络背景
二协议
1初始协议
1.1协议分层
1.2OSI七层模型
1.3TCP/IP五层模型
2再始协议
2.1为什么要有TCP/IP协议
2.2TCP/IP与OS的关系
2.3所以什么是协议
三网络传输基本流程
1局域网(以太网)通信原理
1.1认识mac地址
2同网段主机间收发消息的过程
2.1报文与报头
四跨网络传输流程图
1认识IP地址
2IP地址的意义
3IP地址VSMac地址
编辑
五Socket 编程预备
1IP与进程
2端口号
3传输层
3.1认识Tcp协议
3.2认识Udp协议
4网络字节序
5socket常见接口
socket API
sockaddr 结构
编辑
编码结构
六UDP网络编程
1基础版本
2翻译版本
3聊天室版本
七TCP网络编程
1基础版本
2命令版本
八网络命令
Ping 命令
netstat命令
pidof命令
九不同环境下的通信
1Windows作为client访问Linux(UDP)
2Linux作为clinet访问Windows(TCP)
十connect的断线重连
一计算机网络背景
在网络未出现之前,计算机之间是处于相互独立的状态;
如果要进行小组之前的资源处理,要通过软盘一个一个进行处理
而随着后来有了网络,多台计算机能够连接在一起,完成数据共享
局域网(子网): 通过交换机和路由器连接在一起组成的网段
广域网: 将远隔千里的计算机都连在一起组成的网段
记住:计算机是人的工具, 人要协同工作, 注定了网络的产生是必然的
二协议
1初始协议
比如:在以前上大学时,在外求学的大学生通常是用电话来给家里人进行联系;以前电话费用是按分钟来计算的,与家里人联系时恨不得在1分钟内把要说的话给说完:此时聪明的你在来之前与家里人做了约定:如果打了一次过来后挂了就代表没事,打了两次后挂了就代表没钱了,打了三次挂了就代表有事要联系,需要家里那边打过来...
• 打电话约定电话铃响的次数 == 协议
也就是说:协议是一种约定~
计算机之间的传输媒介是光信号和电信号:通过 "频率" 和 "强弱" 来表示 0 和 1 这样的信息
要想传递各种不同的信息,就需要约定好双方的数据格式
但是:两台通信的主机,只要约定好协议就可以了??
虽然共同知道0 1要表达的电流强弱,但是语言的不同,势必让通信再次成为阻碍~
所以,协议的完善需要更加细致化的规定,更需要参与进来的人遵守
在计算机的世界里:
• 计算机生产厂商有很多
• 计算机操作系统, 也有很多
• 计算机网络硬件设备, 还是有很多
如何让不同厂商之间进行通畅的通信,就需要有人站出来(有着只身实力与众多专利),约定一个共同的标准,让使用的人去遵守这一标准,这就是网络协议
1.1协议分层
协议本质也是软件:在设计上为了更好的进行模块化,解耦合,也是被设计成层状结构
好处:合理的分层可以实现解耦合,让软件维护的成本更低:这也是判断软件好坏之一
软件分层的逻辑在学习语言也同样适用:学习面向对象的三大特点:封装继承多态时,我们要进行资源管理首先要先进行描述:对象的属性特点...在将所有的信息进行组织封装,成为设计中的一层,而继承多态则是对每一层关系的体现,让每一层在解耦合的同时在逻辑上有时紧密的关系~
1.2OSI七层模型
OSI(Open System Interconnection, 开放系统互连) 七层网络模型称为开放式系统互联参考模型, 即是一个逻辑上的定义和规范而已
但OSI终归只是个参考模型,实际上只有5层被实现出来
1.3TCP/IP五层模型
TCP/IP 通讯协议采用了 5 层的层级结构, 每一层都呼叫它的下一层所提供的网络来完成自身需求
• 物理层: 负责光/电信号的传递方式. 比如现在以太网通用的网线(双绞 线)、 早期以太网采用的的同轴电缆(现在主要用于有线电视)、 光纤, 现在的 wifi 无线网使用电磁波等都属于物理层的概念。 物理层的能力决定了最大传输速率、 传输距离、 抗干扰性等. 集线器(Hub)工作在物理层.
• 数据链路层: 负责设备之间的数据帧的传送和识别. 例如网卡设备的驱动、 帧同步(就是说从网线上检测到什么信号算作新帧的开始)、 冲突检测(如果检测到冲突就自动重发)、 数据差错校验等工作. 有以太网、 令牌环网, 无线 LAN 等标准. 交换机(Switch)工作在数据链路层.
• 网络层: 负责地址管理和路由选择. 例如在 IP 协议中, 通过 IP 地址来标识一台主机, 并通过路由表的方式规划出两台主机之间的数据传输的线路(路由). 路由器(Router)工作在网路层.
• 传输层: 负责两台主机之间的数据传输. 如传输控制协议 (TCP), 能够确保数据可靠的从源主机发送到目标主机.
• 应用层: 负责应用程序间沟通, 如简单电子邮件传输(SMTP) 、 文件传输协
议(FTP) 、 网络远程访问协议(Telnet) 等. 我们的网络编程主要就是针对应用层.
物理层我们考虑的比较少, 我们只考虑软件相关的内容. 因此很多时候我们直接称为四层模型
2再始协议
2.1为什么要有TCP/IP协议
首先:即便是单机,你的计算机内部是存在协议的,如内存与其它设备进行通信时的通信协议,只是我们感知不到罢了;这些通信都是在本地主机内进行,距离短,出现问题少~
其次:随着主机间通信距离的变长,大大小小会出现各种问题:
1.我要把 你好啊 数据发送给主机B怎么办? 路由器
2.网络上怎么多主机,怎么准确地给主机B发送过去? IP地址
3.如果数据在发出去的途中丢失了怎么办? 超时重传
4.主机B收到数据,它怎么知道对新到来的进行处理? 协议
而新问题的出现往往伴随着新协议的产生来进行解决
所以:为什么要有TCP/IP协议呢? 本质是通信主机距离变长了;它是一种解决方案
而我们也要知道一个事实:发数据不是目的,而是手段,用数据才是目的
2.2TCP/IP与OS的关系
主机之间操作系统不同,但它们之间还能够互相通信:
1.主机的网络协议栈必须是按照标准进行相同的实现的
2.最重要的协议栈是传输层与网络层是由操作系统来实现的,操作系统再怎么不同大家都是要来遵守的
而两者最著名的相关协议是TCP与IP,所以这个网络协议栈叫做TCP/IP协议不过分吧~
从上图我们可以看出:整个协议栈,既要涉及硬件又要设计OS,甚至用户;所以这个协议一定时要各行IT事业来配合
2.3所以什么是协议
截止到目前, 我们还没接触过任何协议, 但是如何朴素的理解协议, 我们已经可以试试了。
问题: 主机 B 能识别 data, 并且准确提取 a=10, b=20, c=30 吗?
能!因为双方都同样有结构体类型:用“相同”的代码来实现协议,用同样的结构体来定义类型,双方之间不就有天然的“共识”,自然就能识别并提取了:这不就是约定吗!
所以,我们来朴素地理解协议:所谓协议, 就是通信双方都认识的结构化的数据类型
三网络传输基本流程
1局域网(以太网)通信原理
两台主机在同一局域网,能否直接通信?
能!原理就类似上课:在同一间教室里,老师说的话在教室的所有人都能听到(包括老师自己);如果老师点名某某同学起来回答问题时,同学会根据老师的话(数据)来判断站或者不站(身份匹配,名字一般具有唯一性)
而每台主机在局域网中,也是有代表只身唯一性的标识:mac地址
1.1认识mac地址
• MAC 地址用来识别数据链路层中相连的节点;
• 长度为 48 位, 及 6 个字节. 一般用 16 进制数字加上冒号的形式来表示(例如:08:00:27:03:fb:19)
• 在网卡出厂时就确定了, 不能修改. mac 地址通常是唯一的(虚拟机中的 mac 地址不是真实的 mac 地址, 可能会冲突; 也有些网卡支持用户配置 mac 地址)
主机A发数据(你好)个主机E的过程中,主机B,C,D都会收到A发过来的数据,只是它们在对比des的地址不是自己后不对该数据处理(进行丢弃)知道主机E收到数据并判断是主机A给自己的数据后进行处理
注:两个主机的接收数据要理解成两个协议栈收发数据!!
• 以太网中, 任何时刻, 只允许一台机器向网络中发送数据
• 如果有多台同时发送, 会发生数据干扰, 我们称之为数据碰撞
• 所有发送数据的主机要进行碰撞检测和碰撞避免
• 没有交换机的情况下, 一个以太网就是一个碰撞域
• 局域网通信的过程中,主机对收到的报文确认是否是发给自己的:通过目标mac 地址判定•从系统的角度路径以太网:
可把它看成临界资源;而碰撞检测与碰撞避免则类似加锁方式进行保护
2同网段主机间收发消息的过程
其中每层都有协议, 所以在进行上述传输流程的时候, 要进行封装和解包
2.1报文与报头
每层协议都要从上层获取有效载荷(数据)后添加上这层的报头(结构体字段);
报文 = 报头 + 有效载荷
注:数据在网络中发送的时候,一定是要依靠硬件支持
而每一层的协议是多种的:
每一层有这么多协议,在进行解包与分用时是怎么知道要用哪个协议??
这就要认识到网络协议的‘共性’:
1.报头与有效载荷分离的问题——解包
2.除了应用层,每一层协议都解决了一个问题:
自己的有效载荷应该交到上层哪个协议中——分用
能够解决的根本是在封装的过程中添加的报头就代表了这一层的哪个协议
四跨网络传输流程图
1认识IP地址
IP 协议有两个版本, IPv4 和 IPv6. 我们整个的课程, 凡是提到 IP 协议, 没有特殊说明的,
默认都是指 IPv4
• IP 地址是在 IP 协议中, 用来标识网络中不同主机的地址;
• 对于 IPv4 来说, IP 地址是一个 4 字节, 32 位的整数;
• 我们通常也使用 "点分十进制" 的字符串表示 IP 地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;
跨网段的主机的数据传输:数据从一台计算机到另一台计算机传输过程中要经过一个或多个路由器
2IP地址的意义
• 为什么要去目标主机, 先要走路由器?
• 目的 IP 的意义?
来看看一下详细流程图:
用户A要把“你好”的数据发送给用户B时:要依次进行协议栈的封包;
网络层:在封装的报头中有两个IP地址:起始地址与目的地址
数据链路层:封装的报头中有两个mac地址:起始地址与目的地址
再往下就要把报文交给路由器来进行转发(路由器也是主机),而局域网内的连接的其它主机也同时收到该报文(但发现目的mac地址不是自己会丢弃)
路由器收到报文后会进行向上进行解包:在网络层中得到的报文与用户A在网络层的报文时一样的;路由器发现目的地址是在自己的局域网中后,再往下进行封包(数据链路层在封包时的mac地址就要发生改变改变),直接发送给用户B(往上解包得到数据):而解包到网络层时此时的报文与用户A在网络层的报文也是是相同的!
总结:
1.网络层(就是IP层)向上(包括网络层)看到的所有的报文是一样的,都至少是IP报文
2.IP可以屏蔽底层网络的差异
(数据链路层的种类很多:以太网,令牌环网,无线...,但从IP层往上看到的都是一样的)
3.所有的网络都是IP网络
(这也就是为什么手机,电脑...智能设备都能连接网络进行通信的原因)
3IP地址VSMac地址
以唐僧取经为例:
唐僧从长安出发,要前往西天取经时势必要经过很多地方:车迟国,女儿国,火焰山...
到达车迟国时,国王问他:唐僧,你从哪里来,要到哪里去?
唐僧答:国王我从东土大唐来,要到西方去求取真经。上一站是长安,下一站是车迟国:接下来我要到哪里去?
国王想了一下:不就是要去西边嘛,下一站你要去的是女儿国。
知道下一站后,唐僧一行人出发前往女儿国了;在路上时,唐僧想:如果到了女儿国时国王问我从哪来到哪去时,我就是把上一站改成车迟国,下一站改成女儿国就行了...
从这个故事中:我们可以发现:
‘从东土大唐来,到西天去’这个目标是固定不变的,也就是我们所说的IP地址;
而达到每个地方时唐僧的上一站与下一站都在进行改变,自然就是Mac地址;
唐僧到达的每个地方就相当于路由器:告诉我们下一站要去的地方
IP地址在这个过程一直不变不就是唐僧的长远目标吗!
Mac地址不断发生改变不就是在实现长远目标的短期目标(当下目标)吗!
结论:目的 IP 是路径选择的重要依据, mac 地址是局域网转发的重要依据
以下用图来表示网络通信的宏观流程:
五Socket 编程预备
1IP与进程
• IP 在网络中, 用来标识主机的唯一性
但是这里要思考一个问题: 数据传输到主机是目的吗?
不是的。 因为数据是给人用的。比如:聊天是人在聊天,下载是人在下载...
但是人是怎么看到聊天信息的呢? 怎么执行下载任务呢? 怎么浏览网页信息呢?
通过启动的 qq, 迅雷, 浏览器。而启动的 qq, 迅雷, 浏览器都是进程。 换句话说:进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。
所以: 数据传输到主机不是目的, 而是手段。 到达主机内部, 在交给主机内的进程,才是目的。
但是系统中, 同时会存在非常多的进程, 当数据到达目标主机之后, 怎么转发给目标进程?
有人会说:表示进程的唯一性不就是pid吗?的确如此;但在网络中不用pid,而用端口号来表示~
2端口号
端口号(port)是传输层协议的内容
• 端口号是一个 2 字节 16 位的整数;
• IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程;
• 一个端口号只能被一个进程占用
我在手机端打开抖音时,应用层通过:IP + port 标识唯一的一个进程后数据往下进行封装后,通过网络将数据发送到部署抖音的服务器(也是主机)中,往上进行解包分用获取数据,再将处理好的数据往下进行封装通过网络发送我的手机端,往上进行解包获取数据(也就能看到打开后的视频了)
在这个过程中不就是两个进程通过跨网络在进行进程间通信吗!
在这个过程中有一些细节:
0.两个毫不相干的进程在通信时的前提:要看到同一份资源;这里的资源就是:网络
1.一个进程通过网络发送给另一个进程时的前提:携带源IP与目的IP
IP + 端口号 在实现时我们把它叫做套接字socket
2.服务端(客户端)在启动时,一定要和端口号进行绑定(IP已经是有了的)
3.在学习进程时:pid就能标识进程的唯一性,为什么还要有端口号呢?用pid行不行?
a.端口号是多少不重要,端口号用来标识进程唯一性才重要
b.从技术角度上:用pid实现是可能的:本来pid就具有唯一性;但这会有问题:
pid是系统层面的概念:每次启动时是不同的;每次在通信时都要发生改变,造成强耦合性;
有了网络层上的端口号概念:每次启动时先进行绑定后续都不会发送改变(解耦合);
这也是我们在现实生活中:学校为了同一管理给每个分配学号不用身份证来标识唯一性的道理一样,从社会层面与国家层面进行解耦;
而有了端口号:在主机内部那些进程有端口号就表面该进程需要通信管理起来方便~
端口号范围划分:
• 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的端口号都是固定的.
• 1024-65535: 操作系统动态分配的端口号. 客户端程序的端口号就是由操作系统从这个范围分的.
3传输层
如果我们了解了系统, 也了解了网络协议栈, 我们就会清楚, 传输层是属于内核的, 那么我们要通过网络协议栈进行通信, 必定调用的是传输层提供的系统调用, 来进行的网络通信。
3.1认识Tcp协议
此处我们先对 TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识:
• 传输层协议
• 有连接
• 可靠传输
• 面向字节流
3.2认识Udp协议
此处我们也是对 UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识:
• 传输层协议
• 无连接
• 不可靠传输
• 面向数据报
4网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分:在协议中是怎么规定的呢??
• 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
• 接收主机把从网络上接到的字节依次保存在接收缓冲区中,按内存地址从低到高的顺序保存;
• TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节
以下库函数做网络字节序和主机字节序的转换:
5socket常见接口
socket API
C
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockaddr 结构
利用C++中多态属性实现C语言版的多态
编码结构
sockaddr结构
socketaddr_in结构
in_addr结构
六UDP网络编程
1基础版本
云服务器上,服务端不能直接(也强烈不建议)bind 自己的公网IP
服务器IP我们一般指定为0(服务器bind任何IP)(INADDR_ANY)
// UdpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>#include "Log.hpp"
#include "Nocopy.hpp"
#include "InetAddr.hpp"enum
{SOCKET_ERROR = 1,BIND_ERROR
};class UdpServer : public Nocopy
{
public:UdpServer(const uint16_t &localport = 8888, const int &localfd = -1): _localport(localport), _localfd(localfd), _isrunning(false){}void Init(){// 1.创建 --> (网卡打开了)_localfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_localfd < 0){LOG(FATAL, "sockfd create fail\n");exit(SOCKET_ERROR);}LOG(DEBUG, "sockfd create sucess,_sockfd: %d\n", _localfd);// 2.绑定// 2.1填充本地信息struct sockaddr_in local;// 定义出来先清空memset(&local, 0, sizeof(local));// ##左右拼接再替换local.sin_family = AF_INET;// 主机序列要转成网络序列local.sin_port = htons(_localport);// a.需要4字节IP b.需要网络序列的IP// local.sin_addr.s_addr = inet_addr(_locallip.c_str());local.sin_addr.s_addr = INADDR_ANY; // 0// 2.2将填充好的结构体设置进内核中int n = ::bind(_localfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind fail\n");exit(BIND_ERROR);}LOG(DEBUG, "bind sucess\n");}void Start(){_isrunning = true;while (_isrunning){// 接收信息char buffer[1024]; // 收到的信息struct sockaddr_in perr; // 是谁发的socklen_t len = sizeof(perr);ssize_t n = recvfrom(_localfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&perr, &len);if (n < 0){LOG(ERROR, "received fail\n");}buffer[n] = 0;InetAddr addr(perr);// uint16_t perrport = ntohs(perr.sin_port);// std::string perrip = inet_ntoa(perr.sin_addr);std::cout << "[" << addr.Ip() << ' ' << addr.Port() << " ]: " << buffer << std::endl;// 发送消息std::string echo_string = "[client]: ";echo_string += buffer;ssize_t m = sendto(_localfd, echo_string.c_str(), echo_string.size(), 0, (sockaddr *)&perr, len);if (m < 0){std::cerr << "sended fail" << std::endl;}}}~UdpServer(){if (_localfd < 3)close(_localfd);}private:int _localfd;uint16_t _localport; // 端口// std::string _locallip; // ip 后面要特殊处理bool _isrunning;
};// InetAddr.hpp --> 储存客户端的信息
#pragma once
#include<iostream>
#include<string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{void ToHost(){_ip=inet_ntoa(_addr.sin_addr);_port=ntohs(_addr.sin_port);}
public:InetAddr(struct sockaddr_in addr):_addr(addr){ToHost();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}
private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};//NoCopy.hpp --> 不让服务器对象进行拷贝
class Nocopy
{
public:Nocopy(){}~Nocopy(){}Nocopy(const Nocopy&)=delete;Nocopy& operator=(const Nocopy&)=delete;
};
client不让用户自己设定,而是OS随机选择(客户端冲突)
client需要bind自己的IP和端口,但是client 不需要‘显示’bind自己的IP和端口
client首次向server发送数据时,OS自动给clinet bind自己的IP和端口
// UdpClient.hpp
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>#include "Nocopy.hpp"
#include "Log.hpp"// 客户端在未来一定要知道服务端的IP和端口号
class UdpClient : public Nocopy
{
public:UdpClient(const std::string &locallp, uint16_t localport, int socketfd = -1): _locallp(locallp), _localport(localport), _socketfd(socketfd), _isrunning(false){}void Start(){_isrunning = true;// 创建_socketfd = ::socket(AF_INET6, SOCK_DGRAM, 0);if (_socketfd < 0){LOG(FATAL, "create sockfd fail\n");exit(1);}// 客户端不让用户自己设定,而是OS随机选择(客户端冲突)// client需要bind自己的IP和端口,但是client 不需要‘显示’bind自己的IP和端口// client首次向server发送数据时,OS自动给clinet bind自己的IP和端口struct sockaddr_in line;memset(&line, 0, sizeof(line));line.sin_family = AF_INET;line.sin_port = htons(_localport); // 转换line.sin_addr.s_addr = inet_addr(_locallp.c_str()); // 转换while (_isrunning){std::string buffer;std::cout << "Please Write: ";getline(std::cin, buffer);ssize_t n = ::sendto(_socketfd, buffer.c_str(), buffer.size(), 0, (sockaddr *)&line, sizeof(line));if (n < 0){LOG(ERROR,"Sended Fail\n");break;}char message[1024];struct sockaddr_in tmp;socklen_t len=sizeof(tmp);ssize_t m=::recvfrom(_socketfd,&message,sizeof(message)-1,0,(struct sockaddr*)&tmp,&len);if(m<0){LOG(ERROR,"Recvform Fail\n");break;}message[m]=0;std::cout<<message<<std::endl;}}~UdpClient(){if (_socketfd < 3)::close(_socketfd);}private:std::string _locallp;uint16_t _localport;int _socketfd;bool _isrunning;
};
//ServerMain.cc
#include"UdpServer.hpp"//Server-IP 建议为0(收到任意IP)
int main(int args,char* argv[])
{if(args!=2){std::cerr<<"User: "<<argv[0]<<" Server-Port"<<std::endl;exit(1);}uint16_t Port=std::stoi(argv[1]);//std::unique_ptr<UdpServer> user=std::make_unique<UdpServer>(ip); //c++14UdpServer user(Port);user.Init();user.Start();return 0;
}//ClientMain.cc
#include"UdpClient.hpp"int main(int args,char* argv[])
{if(args!=3){std::cerr<<"User: "<<argv[0]<<"Server-Ip Server-Port"<<std::endl;exit(1);}std::string Ip=argv[1];uint16_t Port=std::stoi((argv[2]));//std::unique_ptr<UdpServer> user=std::make_unique<UdpServer>(ip); //c++14UdpClient user(Ip,Port);user.Start();return 0;
}
2翻译版本
将IO逻辑(服务器收到消息)与业务逻辑(进行翻译)进行解耦:借助function函数实现
进行回调时只需通过上层提供的方法进行回调后返回
//UdpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <functional>#include "Log.hpp"
#include "InetAddr.hpp"enum
{SOCKET_ERROR = 1,BIND_ERROR
};using fun_t = std::function<std::string(std::string)>;// IO逻辑 与 业务逻辑 进行解耦
class UdpServer
{
public:UdpServer(fun_t fuc,const uint16_t &localport = 8888, const int &localfd = -1): _fuc(fuc),_localport(localport), _localfd(localfd), _isrunning(false){}void Init(){// 1.创建 --> (网卡打开了)_localfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_localfd < 0){LOG(FATAL, "sockfd create fail\n");exit(SOCKET_ERROR);}LOG(DEBUG, "sockfd create sucess,_sockfd: %d\n", _localfd);// 2.绑定// 2.1填充本地信息struct sockaddr_in local;// 定义出来先清空memset(&local, 0, sizeof(local));// ##左右拼接再替换local.sin_family = AF_INET;// 主机序列要转成网络序列local.sin_port = htons(_localport);// a.需要4字节IP b.需要网络序列的IP// local.sin_addr.s_addr = inet_addr(_locallip.c_str());local.sin_addr.s_addr = INADDR_ANY; // 0// 2.2将填充好的结构体设置进内核中int n = ::bind(_localfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind fail\n");exit(BIND_ERROR);}LOG(DEBUG, "bind sucess\n");}void Start(){_isrunning = true;while (_isrunning){// 接收信息char buffer[1024]; // 收到的信息struct sockaddr_in perr; // 是谁发的socklen_t len = sizeof(perr);ssize_t n = recvfrom(_localfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&perr, &len);if (n < 0){LOG(ERROR, "received fail\n");}buffer[n] = 0;InetAddr addr(perr);std::cout << "[" << addr.Ip() << ' ' << addr.Port() << " ]: " << buffer << std::endl;//进行翻译(回调)std::string result = _fuc(buffer);ssize_t m = sendto(_localfd, result.c_str(), result.size(), 0, (sockaddr *)&perr, len);if (m < 0){LOG(ERROR, "sendto fail\n");}}}~UdpServer(){if (_localfd < 3)close(_localfd);}private:int _localfd;uint16_t _localport; // 端口// std::string _locallip; // ip 后面要特殊处理bool _isrunning;fun_t _fuc;
};
写一个实现翻译功能的对象:
1.从文件中读取单词与单词对应的解释(储存在unordered_map)
2. 获取用户输入的单词在容器中查找并给出结果
//Dict.hpp
#include<iostream>
#include<string>
#include<unordered_map>
#include<fstream>#include"Log.hpp"const static std::string tmp=": ";
class Dict
{
private:void LoadWorld(){std::ifstream in(_path);if(!in.is_open()){LOG(ERROR,"open file fail\n");exit(1);}std::string line;while(std::getline(in,line)){if(line=="") continue;size_t pos=line.find(tmp);if(pos==std::string::npos) continue;std::string key=line.substr(0,pos);if(key=="") continue;std::string valute=line.substr(pos+2);if(valute=="") continue;_dict.insert({key,valute});LOG(DEBUG,"Add %s sucess\n",line.c_str());}LOG(DEBUG,"Add Finish\n");in.close();}
public:Dict(const std::string& path):_path(path){LoadWorld();}std::string Translate(const std::string& world){if(world=="") return "None";auto result=_dict.find(world);if(result==_dict.end()) return "None";else return result->second;}private:std::string _path;std::unordered_map<std::string,std::string> _dict;
};//ServerMain.cc
#include<memory>#include"UdpServer.hpp"
#include"Dict.hpp"int main(int args,char* argv[])
{if(args!=2){std::cerr<<"User: "<<argv[0]<<" Port"<<std::endl;}uint16_t Port=std::stoi(argv[1]);Dict dict("./dict.txt");auto translate=std::bind(&Dict::Translate,&dict,std::placeholders::_1);std::unique_ptr<UdpServer> user=std::make_unique<UdpServer>(translate,Port); //c++14user->Init();user->Start();return 0;
}
3聊天室版本
设计client端时要解决:如果在聊天室有人不发消息时它也能看到别人发的消息
通过双线程(一个收,一个发解决)
//Client.cc#include<functional>
#include "Log.hpp"
#include "UdpClient.hpp"
#include "ThreadPool.hpp"void ReceveMessage(UdpClient *r)
{r->Receve();
}void SendMessage(UdpClient *r)
{r->Send();
}int main(int args, char *argv[])
{if (args != 3){std::cerr << "User: " << argv[0] << "Ip Port" << std::endl;exit(0);}std::string Ip = argv[1];uint16_t Port = std::stoi((argv[2]));UdpClient *r = new UdpClient(Ip, Port);r->Init();// 创建线程进行发送与接收 -- 解决信息积压Thread recever("Thread_Recever", std::bind(&ReceveMessage, r));Thread sender("Thread_Sender", std::bind(&SendMessage, r));recever.Start();sender.Start();recever.Join();sender.Join();return 0;
}//UdpClient.hop
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>#include "Nocopy.hpp"class UdpClient : public Nocopy
{
public:UdpClient(const std::string &locallp, uint16_t localport, int socketfd = -1): _locallp(locallp), _localport(localport), _socketfd(socketfd), _isrunning(false){}int Sockfd(){return _socketfd;}void Init(){_socketfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_socketfd < 0){LOG(ERROR, "socket fail\n");exit(1);}}void Send(){struct sockaddr_in line;memset(&line, 0, sizeof(line));line.sin_addr.s_addr = ::inet_addr(_locallp.c_str()); // 转换line.sin_family = AF_INET;line.sin_port = htons(_localport); // 转换// 发送while (true){std::string buffer;std::cout << "Please Write :";std::getline(std::cin, buffer);ssize_t n = ::sendto(_socketfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&line, sizeof(line));if (n < 0){LOG(ERROR, "sendto fail\n");break;}}}void Receve(){// 接收while (true){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);int n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;// 将消息打到标准错误中std::cerr << buffer << std::endl;}else{LOG(ERROR, "recvfrom error\n");break;}}}~UdpClient(){if (_socketfd < 3)::close(_socketfd);}private:std::string _locallp;uint16_t _localport;int _socketfd;bool _isrunning;
};//InetAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>class InetAddr
{void ToHost(){//_ip=inet_ntoa(_addr.sin_addr);//4字节地址->char* 理论与实践有冲突 不用_port = ntohs(_addr.sin_port);char buffer[124];inet_ntop(AF_INET, &_addr.sin_addr, buffer, sizeof(buffer));_ip = buffer;}public:InetAddr(const struct sockaddr_in &addr): _addr(addr){ToHost();}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}bool operator==(const InetAddr &ad){return this->_ip == ad._ip && this->_port == ad._port;}std::string User(){std::string tmp = _ip + " " + std::to_string(_port);return tmp;}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};
设计Route对象来完成信息转发给所有人(通过线程池进行sendto)
设计时要注意:用来储存在线人数的容器可能存在线程安全问题,要进行加锁保护
(如果没用线程池当我没说)
//Route.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>#include "LockGuard.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"using task_t = std::function<void()>;using namespace threadpool;
class Route
{
public:Route(){pthread_mutex_init(&_mutex,nullptr);}bool Vis(InetAddr &who){LockGuard lock(&_mutex);for (auto &user : _users){if (user == who){return false;}}return true;}void Erase(InetAddr &who){LockGuard lock(&_mutex);auto it = _users.begin();while (it != _users.end()){if ((*it) == who){_users.erase(it);LOG(DEBUG,"Erase [%s]\n",who.User().c_str());break;}it++;}}void Send(int socketfd, const std::string message, InetAddr who) // 线程执行方法{LockGuard lock(&_mutex);std::string send_message="["+who.User()+"]: "+message;for (auto &user : _users){struct sockaddr_in perr = user.Addr();sendto(socketfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&perr,sizeof(user)); }}void Forward(int socketfd, const std::string &message, InetAddr &who) // 外面调用方法{// 用户在不在users里if (Vis(who)){LOG(DEBUG, "Add [%s]\n",who.User().c_str());_users.push_back(who);}// 用户想退出了if (message == "q" || message == "quit"){LOG(DEBUG, "[%s] quit\n",who.User().c_str());Erase(who);}// 用户一定在user中// 收到信息进行发送(单进程执行效率低)// Send(socketfd,message);// 使用多线程进行任务发送(方法绑定)task_t t = std::bind(&Route::Send, this, socketfd, message, who); ThreadPool<task_t>::GetInstance()->Push(t);}~Route(){pthread_mutex_destroy(&_mutex);}private:std::vector<InetAddr> _users;//线程安全(加锁)pthread_mutex_t _mutex;
};//LockGuard.hpp
#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};//Nocopy.hppclass Nocopy
{
public:Nocopy(){}~Nocopy(){}Nocopy(const Nocopy&)=delete;Nocopy& operator=(const Nocopy&)=delete;
};//ThreadPool.hpp
#pragma once#include<queue>#include "Thread.hpp"
using namespace mythread;const static int gnum = 5;
namespace threadpool
{void Lock(pthread_mutex_t &_mx){pthread_mutex_lock(&_mx);}void UnLock(pthread_mutex_t &_mx){pthread_mutex_unlock(&_mx);}template <typename T>class ThreadPool{private:void Sleep(){LOG(DEBUG, "Thread Sleep\n");pthread_cond_wait(&_cond, &_mutex);}void Wake(){LOG(DEBUG, "Thread Wake\n");pthread_cond_signal(&_cond);}void WakeAll(){pthread_cond_broadcast(&_cond);}void Excute(){while (true){Lock(_mutex);while (_task.empty() && _isrunning){_sleep_num++;Sleep();_sleep_num--;}// 没任务要退出了if (_task.empty() && !_isrunning){UnLock(_mutex); // 先解锁LOG(DEBUG, "Thread Quit\n");break;}// 有任务继续执行T t = _task.front();_task.pop();UnLock(_mutex);// 在外面处理任务t();}}ThreadPool(int num = gnum): _num(num), _sleep_num(0), _isrunning(false){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;void operator=(const ThreadPool<T> &) = delete;void Init(){for (int i = 0; i < _num; i++){std::string name = "thread-" + std::to_string(i + 1);_threads.emplace_back(name, std::bind(&ThreadPool::Excute, this));}}void Start(){_isrunning = true;for (auto &thread : _threads){thread.Start();}}public:// 懒汉模式static ThreadPool<T> *GetInstance(){if (_tp == nullptr){LOG(DEBUG, "Creat Threadpool\n");Lock(_sig_mutex);if (_tp == nullptr){_tp = new ThreadPool(); // new 对象出来_tp->Init();_tp->Start();}UnLock(_sig_mutex);}else{LOG(DEBUG, "Get Threadpool\n");}return _tp;}void Push(const T &in){Lock(_mutex);if (_isrunning){_task.push(in);if (_sleep_num > 0)Wake();}UnLock(_mutex);}void Stop(){Lock(_mutex);_isrunning = false;WakeAll();UnLock(_mutex);}void Join(){for(auto& thread:_threads){thread.Join();}}private:int _num;std::vector<Thread> _threads;std::queue<T> _task;bool _isrunning;int _sleep_num;pthread_mutex_t _mutex;pthread_cond_t _cond;static ThreadPool<T> *_tp;static pthread_mutex_t _sig_mutex;};template <typename T>ThreadPool<T> *ThreadPool<T>::_tp = nullptr;template <typename T>pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
}//Thread.hpp
#pragma once
#include<iostream>
#include<functional>
#include<string>#include "Log.hpp"namespace mythread
{//typedef void (*fun_t)(const std::string &name); // 用户线程执行的回调方法using fun_t=std::function<void()>;class Thread{public:void Excute(){_IsRun = true; // 线程开始跑起来了//_result = _func(_name); // 线程结束时返回结果_func();}public:Thread(const std::string &name, const fun_t func): _name(name), _func(func){LOG(DEBUG,"%s created\n", _name.c_str());}static void *ThreadRun(void *args) // 1.设置成static->属于类不属于对象->没有this干扰{//_func(_name);2.写在这里不能访问私有化成员Thread *Self = (Thread *)args; // 获得当前对象Self->Excute(); // 执行用户定义的方法(回调函数)return nullptr;}bool Start(){int n = ::pthread_create(&_ptd, nullptr, ThreadRun, this); // 参数传对象if (n == 0)return true;elsereturn false;}void Stop(){if (_IsRun){pthread_cancel(_ptd);_IsRun = false;LOG(DEBUG,"%s stop\n",_name.c_str());}}void Join(){pthread_join(_ptd, nullptr);LOG(DEBUG,"%s join\n",_name.c_str());}private:pthread_t _ptd;fun_t _func;std::string _result;std::string _name;bool _IsRun;};
}//Log.hpp
#pragma once
#include <pthread.h>
#include <fstream>
#include <syscall.h>
#include <stdarg.h>
#include <unistd.h>
#include <cstring>enum
{DEBUG = 1,INFO,WARNING,ERROR,FATAL
};std::string Getlevel(int level)
{switch (level){case DEBUG:return "DEBUG";break;case INFO:return "INFO";break;case WARNING:return "WARNING";break;case ERROR:return "ERROR";break;case FATAL:return "FATAL";break;default:return "";break;}
}std::string Gettime()
{time_t now = time(nullptr);struct tm *time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",time->tm_year + 1900,time->tm_mon + 1,time->tm_mday,time->tm_hour,time->tm_min,time->tm_sec);return buffer;
}struct log_message
{std::string _level;int _id;std::string _filename;int _filenumber;std::string _cur_time;std::string _message;
};#define SCREAM 1
#define FILE 2#define DEVELOP 3
#define OPERATION 4const std::string gpath = "./log.txt";
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;class log
{
public:log(const std::string &path = gpath, const int status = DEVELOP): _mode(SCREAM), _path(path), _status(status){}void SelectMode(int mode){_mode = mode;}void SelectStatus(int status){_status = status;}void PrintScream(const log_message &le){printf("[%s][%d][%s][%d][%s] %s",le._level.c_str(),le._id,le._filename.c_str(),le._filenumber,le._cur_time.c_str(),le._message.c_str());}void PrintFile(const log_message &le){std::fstream in(_path, std::ios::app);if (!in.is_open())return;char buffer[1024];snprintf(buffer, sizeof(buffer), "[%s][%d][%s][%d][%s] %s",le._level.c_str(),le._id,le._filename.c_str(),le._filenumber,le._cur_time.c_str(),le._message.c_str());in.write(buffer, strlen(buffer)); // 不用sizeofin.close();}void PrintLog(const log_message &le){// 过滤if (_status == OPERATION)return;// 线程安全pthread_mutex_lock(&gmutex);switch (_mode){case SCREAM:PrintScream(le);break;case FILE:PrintFile(le);break;default:break;}pthread_mutex_unlock(&gmutex);}void logmessage(int level, const std::string &filename, int filenumber, const char *message, ...){log_message le;le._level = Getlevel(level);le._id = syscall(SYS_gettid);le._filename = filename;le._filenumber = filenumber;le._cur_time = Gettime();va_list vt;va_start(vt, message);char buffer[128];vsnprintf(buffer, sizeof(buffer), message, vt);va_end(vt);le._message = buffer;// 打印日志PrintLog(le);}~log(){}private:int _mode;std::string _path;int _status;
};// 方便上层调用
log lg;// ##不传时可忽略参数
#define LOG(level, message, ...) \do \{ \lg.logmessage(level, __FILE__, __LINE__, message, ##__VA_ARGS__); \} while (0)#define SleftScream() \do \{ \lg.SelectMode(SCREAM); \} while (0)
#define SleftFile() \do \{ \lg.SelectMode(FILE); \} while (0)#define SleftDevelop() \do \{ \lg.SelectStatus(DEVELOP); \} while (0)
#define SleftOperation() \do \{ \lg.SelectStatus(OPERATION); \} while (0)
接下来是服务器的设计
//UdpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include <functional>#include "Nocopy.hpp"
#include "InetAddr.hpp"enum
{SOCKET_ERROR = 1,BIND_ERROR
};using server_t = std::function<void(int, const std::string &message, InetAddr &who)>; // ?class UdpServer : public Nocopy
{
public:UdpServer(uint16_t localport , server_t fuc,const int &localfd = -1):_localport(localport),_fuc(fuc), _localfd(localfd),_isrunning(false){}void Init(){// 创建_localfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_localfd < 0){LOG(FATAL, "create socket fail\n");exit(SOCKET_ERROR);}LOG(DEBUG, "Create Socket Sucess,Sockfd: %d\n", _localfd);// 绑定struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_localport); // 统一local.sin_addr.s_addr = INADDR_ANY;ssize_t n = bind(_localfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){exit(BIND_ERROR);}LOG(DEBUG, "Bind Success\n");}void Start(){_isrunning = true;char buffer[1024]; // 收到的信息while (_isrunning){// 接收信息struct sockaddr_in perr;socklen_t len = sizeof(perr);ssize_t n = recvfrom(_localfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&perr, &len);if (n > 0){buffer[n] = 0;InetAddr ar(perr);LOG(DEBUG, "[%s]: %s\n", ar.User().c_str(), buffer);// 进行回调(收到的信息发送给所有人)_fuc(_localfd, buffer, ar);}else{LOG(ERROR, "recvfrom error\n");}}_isrunning = false;}~UdpServer(){if (_localfd < 3)close(_localfd);}private:int _localfd;uint16_t _localport; // 端口bool _isrunning;server_t _fuc;
};//Server.cc
#include <memory>#include "Log.hpp"
#include "UdpServer.hpp"
#include "Route.hpp"int main(int args, char *argv[])
{if (args != 2){std::cerr << "User: " << argv[0] << " Port" << std::endl;}uint16_t Port = std::stoi(argv[1]);Route r;server_t fuc = std::bind(&Route::Forward, &r,std::placeholders::_1, std::placeholders::_2,std::placeholders::_3);std::unique_ptr<UdpServer> user = std::make_unique<UdpServer>(Port,fuc); // c++14user->Init();user->Start();return 0;
}
七TCP网络编程
1基础版本
Udp和Tcp网络编程是类似的:不过Tcp在bind后要进行listen监听新连接到来
而listen的返回值我们叫做:sockfd:这与之前socket的返回值一样??
在Tcp中,socket的返回值叫做listensockfd;listen的返回值叫做sockfd;
以一个故事来理清这两者的关系:
在某地有一个鱼店,生意一直很不错,他们的经营模式是:一个服务员在外面拉客;其他服务员(在店内)负责将拉来的客安排到桌子上进行用餐服务;而在外面拉客的服务员可不可能拉客后客人不来他就不拉了??这时不可能的,失败后他会继续等待人进行拉客
所以:这家鱼店的生意可以说大部分是:拉客服务员的功劳
而sockfd就是这个在外面拉客的服务员:listensockfd则是其它服务员辅助sockfd完成连接
// Server.hpp
#pragma once
#include <iostream>
#include <memory>
#include <cstring>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"enum
{SOCK_ERROR = 1,BIND_ERROR,LISTEN_ERROR,WAIT_ERROR
};const static int gsockfd = -1;
const static int gport = 8888;
const static int gbacklog = 8;using task_t = std::function<void()>;
using namespace threadpool;class TcpServer
{
public:TcpServer(const uint16_t port = gport): _port(port),_listensockfd(gsockfd),_isrunning(false){}void Init(){// 创建_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(FATAL, "socket fail\n");exit(SOCK_ERROR);}LOG(DEBUG, "socket sucess , sockfd: %d\n", _listensockfd);// 绑定struct sockaddr_in perr;memset(&perr, 0, sizeof(perr));perr.sin_family = AF_INET;perr.sin_port = htons(_port);perr.sin_addr.s_addr = INADDR_ANY; // 不推荐绑定固定IPif (::bind(_listensockfd, (struct sockaddr *)&perr, sizeof(perr)) < 0){LOG(ERROR, "bind fail\n");exit(BIND_ERROR);}LOG(DEBUG, "bind sucess\n");// 监听 Tcp是面向连接的,未来有连接源源不断来 --> 老板模式if (::listen(_listensockfd, gbacklog) < 0){LOG(ERROR, "listen fail\n");exit(LISTEN_ERROR);}LOG(DEBUG, "listen sucess\n");}void Start(){_isrunning = true;while (_isrunning){// 获取新连接 --> 与Udp的recvfrom类似struct sockaddr_in client;socklen_t len = sizeof(client);// 这里的sockfd 与 _listensockfd 有什么区别?// _listensockfd 只协助accept 获取新连接(每次不一定有新连接到来(阻塞))// sockfd 只提供服务int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept fail\n");// exit(ACCEPT_ERROR);continue; // 没有 继续进行连接}InetAddr addr(client);LOG(DEBUG, "get a line , client: %s\n", addr.User().c_str());// 提供服务// v0 -- (单进程)只能处理单个客户端// Server(sockfd, addr);// v1 -- 多进程// Server_V1(sockfd, addr);// v2 -- 多线程Server_V2(sockfd,addr);// v3 -- 线程池//Server_V3(sockfd,addr);}_isrunning = false;}void Server_V3(int sockfd, InetAddr addr){task_t t=std::bind(&TcpServer::Server,this,sockfd,addr);ThreadPool<task_t>::GetInstance()->Push(t);}void Server_V2(int sockfd, InetAddr addr){pthread_t pid;PthreadDate *date = new PthreadDate(sockfd, this, addr);pthread_create(&pid, nullptr, Excute, date);}struct PthreadDate{int _sockfd;TcpServer *_self;InetAddr _addr;PthreadDate(int sockfd, TcpServer *self, const InetAddr &addr): _sockfd(sockfd),_self(self),_addr(addr){}};static void *Excute(void *args){pthread_detach(pthread_self());// static 问题1;无法调用类内方法 2:sockfd变量看不到PthreadDate *date = static_cast<PthreadDate *>(args);date->_self->Server(date->_sockfd, date->_addr);delete date;return nullptr;}void Server_V1(int sockfd, InetAddr addr){pid_t n = fork();if (n == 0){// child// 建议:1.防止子进程误操作 2.fd表有上限::close(_listensockfd);if (fork() > 0)exit(0);// grandchildServer(sockfd, addr);exit(0);}// father::close(sockfd); // 建议 类似pid_t m = ::waitpid(n, nullptr, 0);if (m > 0){LOG(DEBUG, "wait sucess\n");}}void Server(int sockfd, InetAddr addr){while (true){// 收消息 -- 类似读写来收发char buffer[1024];ssize_t n = ::read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){// 发buffer[n] = 0;std::string send = "[server echo]: ";send += buffer;if (::write(sockfd, send.c_str(), send.size()) < 0){std::cerr << "write fail" << std::endl;}}else if (n == 0){LOG(DEBUG, "%s quit\n", addr.User().c_str());break;}else{LOG(ERROR, "read fail\n");break;}}}private:uint16_t _port;int _listensockfd;bool _isrunning;
};//ServerMian.cc
#include<memory>#include"Server.hpp"int main(int args,char* argv[])
{if(args!=2){std::cerr<<"./Server Localport"<<std::endl;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<TcpServer> sr=std::make_unique<TcpServer>(port);sr->Init();sr->Start();return 0;
}//ClientMain.cc
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>int main(int args, char *argv[])
{if (args != 3){std::cerr << "./Server ip sort" << std::endl;exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket fail" << std::endl;exit(2);}std::cout << "socket sucess" << std::endl;// 连接 --> 不需要显示bind 但一定要自己的Ip和port来填充struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);::inet_pton(AF_INET, ip.c_str(), &server.sin_addr);int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect fail" << std::endl;exit(2);}std::cout << "connect success" << std::endl;// 操作while (true){// 发std::string message;std::cout << "Enter: ";getline(std::cin, message);if (::write(sockfd, message.c_str(), message.size()) < 0){std::cerr << "wirte fail" << std::endl;}// 收char buffer[1024];n = ::read(sockfd, buffer, sizeof(buffer)-1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}else{break;}}return 0;
}
在设计Server时,共有4中版本:单进程,多进程,多线程,线程池
其中最优的是:多线程的版本~
2命令版本
实现一个业务:让client通过输入指令,让server端收到并执行该指令
//Server.hpp
#pragma once
#include <iostream>
#include <memory>
#include <cstring>
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>#include "InetAddr.hpp"
#include "Log.hpp"enum
{SOCK_ERROR = 1,BIND_ERROR,LISTEN_ERROR,WAIT_ERROR
};const static int gsockfd = -1;
const static int gport = 8888;
const static int gbacklog = 8;using comm_t = std::function<void(int,InetAddr)>;class TcpServer
{
public:TcpServer(comm_t command,const uint16_t port = gport):_command(command), _port(port),_listensockfd(gsockfd),_isrunning(false){}void Init(){// 创建_listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0){LOG(FATAL, "socket fail\n");exit(SOCK_ERROR);}LOG(DEBUG, "socket sucess , sockfd: %d\n", _listensockfd);// 绑定struct sockaddr_in perr;memset(&perr, 0, sizeof(perr));perr.sin_family = AF_INET;perr.sin_port = htons(_port);perr.sin_addr.s_addr = INADDR_ANY; // 不推荐绑定固定IPif (::bind(_listensockfd, (struct sockaddr *)&perr, sizeof(perr)) < 0){LOG(ERROR, "bind fail\n");exit(BIND_ERROR);}LOG(DEBUG, "bind sucess\n");// 监听 Tcp是面向连接的,未来有连接源源不断来 --> 老板模式if (::listen(_listensockfd, gbacklog) < 0){LOG(ERROR, "listen fail\n");exit(LISTEN_ERROR);}LOG(DEBUG, "listen sucess\n");}void Start(){_isrunning = true;while (_isrunning){// 获取新连接 --> 与Udp的recvfrom类似struct sockaddr_in client;socklen_t len = sizeof(client);// 这里的sockfd 与 _listensockfd 有什么区别?// _listensockfd 只协助accept 获取新连接(每次不一定有新连接到来(阻塞))// sockfd 只提供服务int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len);if (sockfd < 0){LOG(WARNING, "accept fail\n");// exit(ACCEPT_ERROR);continue; // 没有 继续进行连接}InetAddr addr(client);LOG(DEBUG, "get a line , client: %s\n", addr.User().c_str());// 提供服务// v2 -- 多线程Server_V2(sockfd,addr);}_isrunning = false;}void Server_V2(int sockfd, InetAddr addr){pthread_t pid;PthreadDate *date = new PthreadDate(sockfd, this, addr);pthread_create(&pid, nullptr, Excute, date);}struct PthreadDate{int _sockfd;TcpServer *_self;InetAddr _addr;PthreadDate(int sockfd, TcpServer *self, const InetAddr &addr): _sockfd(sockfd),_self(self),_addr(addr){}};static void *Excute(void *args){pthread_detach(pthread_self());// static 问题1;无法调用类内方法 2:sockfd变量看不到PthreadDate *date = static_cast<PthreadDate *>(args);date->_self->_command(date->_sockfd, date->_addr);delete date;return nullptr;}private:comm_t _command;uint16_t _port;int _listensockfd;bool _isrunning;
};//Command.hpp
#pragma once#include <cstdio>
#include "Server.hpp"class command
{
public:command() {}~command() {}std::string Excute(const std::string &buffer){std::string result;// 函数内部创建子进程,将command写进管道内 父进程通过fp读取管道内的数据FILE *fp = popen(buffer.c_str(), "r");if (fp){// 打开成功char line[1024];while (fgets(line, sizeof(line), fp)){result += line;}return result.empty() ? "sucess" : result;//touch 返回无结果}return "popen fail";}void servert(int sockfd, InetAddr addr){while (true){// 收消息 -- 类似读写来收发char CommandBuf[1024];ssize_t n = ::recv(sockfd, CommandBuf, sizeof(CommandBuf) - 1, 0);if (n > 0){CommandBuf[n] = 0;LOG(DEBUG, "get command from client %s command: %s\n", addr.User().c_str(), CommandBuf);std::string result = Excute(CommandBuf);::send(sockfd, result.c_str(), result.size(), 0);}else if (n == 0){LOG(DEBUG, "%s quit\n", addr.User().c_str());break;}else{LOG(ERROR, "read fail\n");break;}}}
};//ServerMain.cc
#include <memory>
#include <functional>
#include "Command.hpp"
int main(int args, char *argv[])
{if (args != 2){std::cerr << "./Server Localport" << std::endl;}uint16_t port = std::stoi(argv[1]);command c;std::unique_ptr<TcpServer> sr = std::make_unique<TcpServer>(std::bind(&command::servert,&c, std::placeholders::_1, std::placeholders::_2),port);sr->Init();sr->Start();return 0;
}//ClientMain.cc
#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>int main(int args, char *argv[])
{if (args != 3){std::cerr << "./Server ip sort" << std::endl;exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 创建int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket fail" << std::endl;exit(2);}std::cout << "socket sucess" << std::endl;// 连接 --> 不需要显示bind 但一定要自己的Ip和port来填充struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(port);::inet_pton(AF_INET, ip.c_str(), &server.sin_addr);int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << "connect fail" << std::endl;exit(2);}std::cout << "connect success" << std::endl;// 操作while (true){// 发std::string message;std::cout << "Enter: ";getline(std::cin, message);if (::write(sockfd, message.c_str(), message.size()) < 0){std::cerr << "wirte fail" << std::endl;}// 收char buffer[1024];n = ::read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}else{std::cerr << "read fail" << std::endl;break;}}::close(sockfd);return 0;
}
更严谨的设计:要把危险的指令,如rm -f /* 等等指令给禁掉(或者添加白名单)
八网络命令
Ping 命令
主要作用:
1、用来检测网络的连通情况和分析网络速度
2、根据域名得到服务器IP
3、根据ping返回的TTL值来判断对方所使用的操作系统及数据包经过路由器数量
$ ping -c 1 www.qq.com
PING ins-r23tsuuf.ias.tencent-cloud.net (121.14.77.221) 56(84) bytes of data.
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=1 ttl=128 time=44.0 ms--- ins-r23tsuuf.ias.tencent-cloud.net ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 44.029/44.029/44.029/0.000 ms
netstat命令
语法: netstat [选项]
功能: 查看网络状态
常用选项:
• a (all)显示所有选项, 默认不显示 LISTEN 相关
• u (udp)仅显示 udp 相关选项
• p 显示建立相关链接的程序名
• n 拒绝显示别名, 能显示数字的全部转化成数字
• t (tcp)仅显示 tcp 相关选项
• l 仅列出有在 Listen (监听) 的服務状态• p 显示建立相关链接的程序名
• n 拒绝显示别名, 能显示数字的全部转化成数字
// 每个 1s 执行一次 netstat -tlnp
$ watch -n 1 netstat -tlnp
pidof命令
在查看服务器的进程 id 时非常方便.
语法: pidof [进程名]
功能: 通过进程名, 查看进程 id
$ ps axj | head -1 && ps ajx | grep tcp_server
PPID PID PGID SID TTY TPGID STAT UID TIME
COMMAND
2958169 2958285 2958285 2958169 pts/2 2958285 S+ 1002
0:00 ./tcp_server 8888$ pidof tcp_server
2958285
$ pidof tcp_server | xargs kill -9 //直接kill掉tcp_server
九不同环境下的通信
在进行通信之前:如果是云服务器,一定要在后台开发端口号!
腾讯云:
阿里云:
1Windows作为client访问Linux(UDP)
// UdpServer.cc
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;enum
{Usage_Err = 1,Socket_Err,Bind_Err
};class UdpServer
{
public:UdpServer(uint16_t port = defaultport): _port(port), _sockfd(defaultfd){}void Init(){// 1. 创建socket,就是创建了文件细节_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){exit(Socket_Err);}// 2. 绑定,指定网络信息struct sockaddr_in local;bzero(&local, sizeof(local)); // memsetlocal.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY; // 1. 4字节IP 2. 变成网络序列// 结构体填完,设置到内核中了吗??没有int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n != 0){exit(Bind_Err);}}void Start(){// 服务器永远不退出char buffer[defaultsize];for (;;){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){uint16_t clientport = ntohs(peer.sin_port);std::string clientip = inet_ntoa(peer.sin_addr);std::string prefix = clientip + ":" + std::to_string(clientport);buffer[n] = 0;std::cout << prefix << "# " << buffer << std::endl;std::string echo = buffer;echo += "[udp server echo message]";sendto(_sockfd, echo.c_str(), echo.size(), 0, (struct sockaddr *)&peer, len);}}}~UdpServer(){}private:uint16_t _port;int _sockfd;
};void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n"<< std::endl;
}// ./udp_server 8888
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);usvr->Init();usvr->Start();return 0;
}//UdpClient.cc
#include <iostream>
#include <cstdio>
#include <thread>
#include <string>
#include <cstdlib>
#include <WinSock2.h>
#include <Windows.h>//屏蔽不安全
#pragma warning(disable : 4996)#pragma comment(lib, "ws2_32.lib")std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号int main()
{//-------------//找库版本WSADATA wsd;WSAStartup(MAKEWORD(2, 2), &wsd);//-------------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());SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd == SOCKET_ERROR){std::cout << "socker error" << std::endl;return 1;}std::string message;char buffer[1024];while (true){std::cout << "Please Enter@ ";std::getline(std::cin, message);if(message.empty()) continue;sendto(sockfd, message.c_str(), (int)message.size(), 0, (struct sockaddr *)&server, sizeof(server));struct sockaddr_in temp;int len = sizeof(temp);int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;std::cout << buffer << std::endl;}}//------------------closesocket(sockfd);WSACleanup();//------------------return 0;
}
2Linux作为clinet访问Windows(TCP)
//TcpServer.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <unistd.h>const static int default_backlog = 6;enum
{Usage_Err = 1,Socket_Err,Bind_Err,Listen_Err
};#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)class TcpServer
{
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){exit(0);}int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 2. 填充本地网络信息并bindstruct 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 bindif (bind(_listensock, CONV(&local), sizeof(local)) != 0){exit(Bind_Err);}// 3. 设置socket为监听状态,tcp特有的if (listen(_listensock, default_backlog) != 0){exit(Listen_Err);}}void ProcessConnection(int sockfd, struct sockaddr_in &peer){uint16_t clientport = ntohs(peer.sin_port);std::string clientip = inet_ntoa(peer.sin_addr);std::string prefix = clientip + ":" + std::to_string(clientport);std::cout << "get a new connection, info is : " << prefix << std::endl;while (true){char inbuffer[1024];ssize_t s = ::read(sockfd, inbuffer, sizeof(inbuffer)-1);if(s > 0){inbuffer[s] = 0;std::cout << prefix << "# " << inbuffer << std::endl;std::string echo = inbuffer;echo += "[tcp server echo message]";write(sockfd, echo.c_str(), echo.size());}else{std::cout << prefix << " client quit" << std::endl;break;}}}void Start(){_isrunning = true;while (_isrunning){// 4. 获取连接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0){continue;}ProcessConnection(sockfd, peer);}}~TcpServer(){}private:uint16_t _port;int _listensock; // TODObool _isrunning;
};using namespace std;void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n"<< std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{if (argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = stoi(argv[1]);std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);tsvr->Init();tsvr->Start();return 0;
}//TcpClient.cc#include <iostream>
#include <string>
//---------------------------------
//库版本
#include <winsock2.h>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
//---------------------------------
std::string serverip = ""; // 填写你的云服务器ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号int main()
{//-----------------------//WSADATA: 保存初始化 Winsock 库时返回的信息WSADATA wsaData;int result = WSAStartup(MAKEWORD(2, 2), &wsaData);if (result != 0){std::cerr << "WSAStartup failed: " << result << std::endl;return 1;}//-----------------------SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (clientSocket == INVALID_SOCKET){std::cerr << "socket failed" << std::endl;WSACleanup();return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(serverport); // 替换为服务器端口serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str()); // 替换为服务器IP地址result = connect(clientSocket, (SOCKADDR *)&serverAddr, sizeof(serverAddr));if (result == SOCKET_ERROR){std::cerr << "connect failed" << std::endl;closesocket(clientSocket);WSACleanup();return 1;}while (true){std::string message;std::cout << "Please Enter@ ";std::getline(std::cin, message);if(message.empty()) continue;send(clientSocket, message.c_str(), message.size(), 0);char buffer[1024] = {0};int bytesReceived = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);if (bytesReceived > 0){buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾std::cout << "Received from server: " << buffer << std::endl;}else{std::cerr << "recv failed" << std::endl;}}//------------------------closesocket(clientSocket);WSACleanup();//------------------------return 0;
}
十connect的断线重连
设计时要基于状态机来实现逻辑
void Excute(){while (true){switch (_connect.ConnectStatus()){case status::NEW: _connect.Connect();break;case status::CONNECTED:_connect.Process();break;case status::DISCONNECT:_connect.Reconnect();break;case status::CLOSED:_connect.Disconnect();return; // 退出default:break;}}}
根据它来进行设计clinet
#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>enum class status
{NEW,CONNECTING,CONNECTED,DISCONNECT, // 断线重连CLOSED // 无法连接
};enum
{SOCKETERROR = 1,CONNECTERROR,ERROR
};const static int DefaultSockfd = -1;
const static int DefaultRetryInterval = 1;
const static int DefaultMaxRetries = 5;class Client_Connect
{
public:Client_Connect(std::string ip, uint16_t port): _ip(ip), _port(port), _sockfd(DefaultSockfd), _status(status::NEW), _retry_interval(DefaultRetryInterval), _max_retries(DefaultMaxRetries){}~Client_Connect(){}status ConnectStatus(){return _status;}void Connect() //正常连接{_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){std::cerr << "create sockfd fail" << std::endl;exit(SOCKETERROR);}struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_port);inet_pton(AF_INET, _ip.c_str(), &server.sin_addr);int n = connect(_sockfd, (const sockaddr *)&server, sizeof(server));if (n < 0){// 1.conncet失败,不代表创建socket失败(先关闭sockfd)Disconnect();// 2.设置状态 -- 没有连接成功(重连)_status = status::DISCONNECT;// 3.返回return;}_status = status::CONNECTED;}void Reconnect() //重新连接{_status = status::CONNECTING;int cnt = 0;while (true){Connect();if (_status == status::CONNECTED){break;}cnt++;if (cnt > _max_retries){// 连接失败进行关闭_status = status::CLOSED;std::cout << "connect fail" << std::endl;break;}std::cout << "reconnecting cnt:" << cnt << std::endl;sleep(_retry_interval);}}void Process() // IO通信{while (true){std::string message = "hello server";ssize_t n = send(_sockfd, message.c_str(), message.size(), 0);if (n > 0){char buffer[1024];ssize_t m = recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}else{_status = status::DISCONNECT;break;}}else{std::cerr << "send fail" << std::endl;_status = status::DISCONNECT;// _status = status::CLOSED;break;}sleep(1);}}void Disconnect() //关闭连接 {if (_sockfd > DefaultSockfd){close(_sockfd);_sockfd = DefaultSockfd;_status = status::CLOSED;}}private:std::string _ip;uint16_t _port;int _sockfd;status _status;int _retry_interval;int _max_retries;
};class TcpClient
{
public:TcpClient(std::string ip, uint16_t port): _connect(ip, port){}~TcpClient(){}void Excute(){while (true){switch (_connect.ConnectStatus()){case status::NEW:_connect.Connect();break;case status::CONNECTED:_connect.Process();break;case status::DISCONNECT:_connect.Reconnect();break;case status::CLOSED:_connect.Disconnect();return; // 退出default:break;}}}private:Client_Connect _connect;
};void Usage(std::string proc)
{std::cout << "Usage : " << proc << " ip port" << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(ERROR);}std::string Ip = argv[1];uint16_t Port = std::stoi(argv[2]);// 可以使用多线程来进行重连逻辑std::unique_ptr<TcpClient> tsvr = std::make_unique<TcpClient>(Ip, Port);tsvr->Excute();return 0;
}
服务器可以用上面的不同环境下Linux作为server的测试代码
现象
以上便是全部内容,有错误欢迎指正,感谢~