文章目录
- 1.引入
- 2. 静态成员函数
- 3.TCP:传输控制协议
- 4.守护进程
- 4.0前台进程
- 4.1介绍
- 4.2认识
- 4.3会话
- 4.3ps axj
- 4.4理解
- 4.5/dev/null
- 4.6守护进程和孤儿进程
- 5.JSON
- 6.完整代码
- 6.1Makefile
- 6.2Socket.hpp
- 6.3Protocol.hpp
- 6.4Log.hpp
- 6.5Daemon.hpp
- 6.6TcpServer.hpp
- 6.7Client.cc
- 6.8Server.cc
1.引入
- 前面所讲的udp/tcp编程属于应用层开发
- 之前讲的套接字没有涉及到协议定制的问题 只是用套接字用来数据的收发
结构体协议:
可扩展性差:修该op可能导致整个协议得跟着变
平台不一致:大小端/内存对齐 不同平台对struct的定义不同 截取二进制时易乱码
结构化的数据 – 序列化的数据 – 字节流
tcp/udp:内核级的协议传的是结构化的数据 :条件编译识别平台 对大小端/内存对齐都做了约定
序列化:减少通信成本 降低应用层编码难度 结构化的数据一般给上层业务使用 字节流比较适合网络传输 ==》 业务逻辑和网络通信的解耦!
字段本身就是协议的一部分!:字段代表什么含义
2. 静态成员函数
独立于任何特定的对象实例:静态成员函数不依赖于特定的对象实例,因此可以直接通过类名来调用,而无需创建类的对象实例。
无法访问非静态成员变量或函数:静态成员函数只能访问静态成员变量和静态成员函数,无法直接访问类的非静态成员变量或函数。这是因为非静态成员变量或函数的访问需要特定的对象实例,而静态成员函数没有与之关联的实例。
可以直接访问类的静态成员变量和函数:静态成员函数可以直接访问类的静态成员变量和函数,因为它们都属于类而不是对象实例。
不具有this指针:静态成员函数不具有隐含的this指针,因为它们不与特定的对象实例相关联。因此,在静态成员函数中不能使用this指针来访问对象的成员变量或函数。
用于实现与类相关的全局函数或工具函数:静态成员函数常用于实现与类相关的全局函数或工具函数,这些函数不依赖于任何特定的对象实例,而只与类本身相关。
总的来说,静态成员函数是类的一部分,但它们与特定的对象实例无关,并且通常用于执行与类相关的操作而不是特定对象的操作
3.TCP:传输控制协议
- 调用的send/recv并不是直接把数据发到/接收 网络或主机
- 什么时候发/发多少/出错了怎么办 由TCP控制
- 客户端把发送的信息存入自己定义的clientBuf,然后调用send,实际上是把clientBuf的数据拷贝到一个发送缓冲区,recv也一样,把数据从接收缓冲区拷贝到服务端定义的serverBuf==》IO接口本质是拷贝函数
- 发送的次数和接收的次数无关。若clientBuf大发送缓冲区小,clientBuf要向发送缓冲区拷贝多次才能是一个完整的数据。服务端同。
- 缓冲区什么时候向网络发:这是TCP控制的!
之前写的tcp代码都是不完善的!
- UDP面向数据报,而TCP面向字节流,并没有对【发送的数据格式和接收的数据格式】做控制。udp即便发很多,读的时候也是一个一个读。tcp不会。
- 如何保证读到的数据是以一个完整的数据?
_left _op _right\r\n
这样能保证吗?识别到特殊字符即为一个数据,可以吗?不可以!如果正文里就含有特殊字符怎么办?length\r\n_left _op _right\r\n
:length是一个整数,他绝对不会包含\r\n,length告知了一个完整的数据有多长,读数据时读”这么长“的数据即可。length后的\r\n用来区分【协议报头和有效载荷】;_right后的\r\n用来增强数据可读性,不加也可以。length不包括\r\n。 - 有了数据格式的规定,再来谈数据的读取。一次只读一个完整数据读不到就等待:不采取,效率低。一次把缓冲区的数据都都进来之后按照规定好的格式去解析读取:采取,效率高。
4.守护进程
4.0前台进程
Linux下什么叫前台进程,与后台进程有什么区别
命令行下能处理你输入命令的进程叫前台进程。BASH就是。
在Linux系统中,前台进程是指在终端活动和用户输入下运行的进程,用户可以在终端中输入命令并直接与之交互。具体来说,前台进程是在前台运行的程序,用户可以看到程序的输出,并且可以通过键盘输入与程序进行交互。
与此相对,后台进程则是在后台默默运行的程序,不受用户当前操作的影响。后台进程也叫守护进程(Daemon),通常不受终端控制,也不需要终端的交互。Linux的大多数服务器就是使用守护进程实现的,如Web服务器的httpd等。这些进程即便在用户关闭终端后也会继续运行。
总的来说,前台进程和后台进程的主要区别在于是否与用户直接交互以及是否受终端的控制。前台进程直接与用户交互并受终端控制,而后台进程(守护进程)则独立运行,不受用户当前操作和终端的控制。
此外,前台进程和后台进程可以通过一些命令进行互相转换。例如,在Linux终端中执行的命令后加上“&”符号,可以把该命令放到后台执行。同样,也可以使用“fg”命令将后台中的命令调至前台继续运行。
请注意,对于需要长时间运行的任务或者不希望被打断的任务,通常会将其放到后台运行。而对于需要实时查看输出或者需要与用户交互的任务,则通常在前台运行。
4.1介绍
在Linux系统中,守护进程(Daemon)是一种在后台运行并提供某种服务的进程。它们通常在系统启动时开始运行,并持续运行直到系统关闭。守护进程是系统的重要组成部分,负责处理系统任务、监听网络请求、管理硬件资源等。
一、守护进程的特点
后台运行:守护进程在后台运行,不占用前台终端窗口,因此用户通常看不到它们的运行过程。
持续运行:守护进程在系统运行时一直存在,除非被显式停止或系统关闭。
提供服务:守护进程通常负责提供某种服务,如Web服务、数据库服务、文件共享服务等。
响应请求:守护进程能够监听并响应来自客户端的请求,如网络请求、文件操作请求等。
二、守护进程的创建与管理
- 创建守护进程
在Linux中,可以通过多种方式创建守护进程,例如使用系统服务管理工具(如systemd、init.d等)或编写自定义的守护进程程序。自定义守护进程程序通常涉及以下步骤:
(1)在程序中调用fork()创建一个子进程,并让父进程立即退出。这样,子进程将成为一个孤儿进程,被init进程(PID为1的进程)接管。
(2)在子进程中调用setsid()创建一个新的会话,并成为会话的领头进程。这将使子进程成为一个新的进程组的领头进程,并且没有控制终端。
(3)关闭不必要的文件描述符,重定向标准输入、输出和错误输出到/dev/null或指定的日志文件。
(4)更改当前工作目录到一个合适的路径,如“/”或“/tmp”,以防止守护进程占用的挂载点被卸载而导致问题。
(5)设置文件权限掩码,以确保守护进程创建的文件具有正确的权限。
(6)进入无限循环,执行守护进程的主要任务。
- 管理守护进程
Linux提供了多种工具来管理守护进程,如systemctl、service、init.d脚本等。这些工具可以启动、停止、重启和查询守护进程的状态。例如,使用systemctl命令可以方便地管理systemd管理的服务:
(1)启动服务:systemctl start 服务名
(2)停止服务:systemctl stop 服务名
(3)重启服务:systemctl restart 服务名
(4)查看服务状态:systemctl status 服务名
此外,还可以使用ps、top等命令查看系统中运行的守护进程及其状态。
三、守护进程的安全性
由于守护进程在系统后台运行并具有特定的权限,因此其安全性至关重要。以下是一些提高守护进程安全性的建议:
使用非root用户运行守护进程,以减少潜在的安全风险。
限制守护进程对网络、文件系统和其他资源的访问权限。
对守护进程的输入进行验证和过滤,以防止注入攻击等安全漏洞。
定期更新和修补守护进程及其依赖的库和工具,以修复已知的安全漏洞。
总之,Linux下的守护进程是系统的重要组成部分,负责提供各种服务并处理系统任务。了解守护进程的特点、创建与管理方法以及安全性建议,有助于更好地使用和管理Linux系统。
setsid
daemon
为什么程序员不使用daemon而自己编写代码来让进程变成守护进程
程序员选择自己编写代码来让进程变成守护进程,而不是直接使用系统提供的daemon功能,主要有以下几个原因:
自定义和灵活性:不同的应用程序和服务可能需要不同的守护进程行为。自己编写代码可以更精确地控制守护进程的启动、运行和退出行为,以满足特定需求。同时,通过编程,可以方便地集成其他功能,如日志记录、性能监控等。
跨平台兼容性:不同的操作系统和平台可能具有不同的daemon或守护进程实现方式。自己编写代码可以确保跨平台的兼容性,使得程序可以在多个操作系统上运行,而无需对守护进程部分进行大量修改。
控制和监控:通过自己编写代码,程序员可以更好地控制和监控守护进程的状态和行为。他们可以添加自定义的监控机制,以便在守护进程出现问题时及时得到通知,并采取相应的措施。
学习和成长:自己编写守护进程代码对于程序员来说是一个很好的学习和成长机会。通过深入了解守护进程的工作原理和编程技术,他们可以提升自己的技能水平,并更好地应对未来的挑战。
需要注意的是,虽然自己编写代码可以带来更高的灵活性和自定义程度,但同时也增加了开发和维护的复杂度。在选择是否自己编写守护进程代码时,程序员需要权衡这些因素,并根据项目的实际需求做出决策。此外,对于一些常见的守护进程需求,使用现有的系统工具或库也是一个很好的选择,可以节省开发时间和提高代码质量。
怎么干掉守护进程?
- 设置信号捕捉函数,如发送2号信号就杀死他。
- 直接kill -9
4.2认识
- 前台进程: 和终端关联的进程。
- 任何xshell 登陆,只允许一个前台进程和多个后台进程
- 进程除了有自己的pid,ppid,还有一个组ID:GID
- 在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程都是bash->可以用匿名管道来进行通信
- 同时被创建的多个进程可以成为一个进程组,组长一般是第一个进程
- 任何一次登陆,登陆的用户,需要有多个进程(组),来给这个用户提供服务的(bash),用户自己可以启动很多进程,或者进程组。给用户提供服务的进程或者用户自己启动的进程,整体属于“会话”。
- 守护进程不能直接向显示器打印消息 一旦打印,会被暂停/终止
4.3会话
Linux下的会话(Session)是指用户在Linux操作系统中与系统进行交互的一种方式。会话是用户登录到系统后启动的进程集合,可以包含一个或多个进程,这些进程又可以包含一个或多个作业。每个会话都有一个唯一的会话ID,用于标识该会话。
会话具有以下几个特点:
交互性:会话是用户与系统进行交互的平台,用户可以通过命令行或图形界面输入指令、操作文件等。
持久性:会话可以持续存在一段时间,用户可以在会话中多次执行指令,直到退出会话或系统重启。
独立性:每个会话是相互独立的,不会互相干扰。用户可以同时启动多个会话,分别执行不同的任务。
在Linux中,用户可以通过命令行终端、SSH远程登录等方式来创建会话。会话有两种类型:前台会话和后台会话。前台会话是用户正在操作的会话,用户可以通过命令行终端或图形界面与前台会话进行交互。后台会话是用户在执行某些任务时创建的会话,这些任务在后台运行,用户可以在后台会话执行任务的同时继续进行其他操作。
此外,Linux还提供了一些会话管理命令,如who命令来显示当前登录到系统的用户和会话信息,exit命令来退出当前会话等。同时,还有如Screen和Tmux这样的会话管理工具,可以帮助用户更方便地管理会话,例如断开与恢复会话、列出与切换会话等。
总的来说,Linux会话是Linux操作系统中非常重要的概念,它提供了用户与系统交互的一种方式,通过会话控制和共享,可以管理系统资源和进程,提高系统的利用率。
4.3ps axj
PPID (Parent Process ID):
表示创建当前进程的父进程的进程ID。每个进程都是由另一个进程创建的,这个创建者就是父进程。
PID (Process ID):
进程的唯一标识符。每个进程在系统中都有一个唯一的进程ID。
GID (Group ID):
进程所属的实际组ID。在Unix和Linux系统中,进程不仅属于一个用户,还属于一个或多个组。
SID (Session ID):
会话的标识符。会话是一组共享同一个控制终端的进程。会话的第一个进程(通常是登录shell)是会话领导者,其PID也是SID。
TTY (Teletype):
进程关联的控制终端的名称。这通常是进程与其交互的终端设备的名称。如果进程没有与任何终端关联(例如后台进程),则此字段可能显示为?
。
TPGID (TTY Process Group ID):
与进程关联的终端进程组ID。进程组是一组共享同一终端输入和输出的进程。TPGID标识了这个进程组中前台进程的PID。
STAT (Process Status):
进程的状态。常见的状态有:
R (运行): 正在运行或在运行队列中等待。
S (休眠): 休眠状态,等待某个条件成立。
Z (僵尸): 终止状态,父进程尚未回收其资源。
T (停止): 进程被停止。
D (不可中断的休眠): 通常是在进行I/O操作时。
以及其他状态…
UID (User ID):
运行进程的用户ID。每个进程都有一个与之关联的用户,该用户拥有运行该进程的所有权限。
TIME:
进程使用的总CPU时间,通常以分钟和秒的形式表示。这表示该进程自启动以来消耗的总CPU时间。
COMMAND:
启动进程的命令行。这通常是启动进程时使用的完整命令行。
这些字段之间的联系体现在它们共同描述了进程在系统中的状态、属性以及与其他进程和系统的交互方式。例如,PPID 和 PID 一起描述了进程之间的父子关系;GID 和 UID 描述了进程的安全上下文;TTY 和 TPGID 描述了进程与终端的交互方式;而 STAT 和 TIME 则提供了进程当前状态和资源使用情况的信息。
4.4理解
- xshell下可以创建多个会话。会话中的进程在退出登录时不一定结束:不同OS处理不同。
- 守护进程是孤儿进程的一种,孤儿进程属于原来环境下的会话,而守护进程属于一个独立的新的会话。
- 在Unix和Linux操作系统中,sid(Session ID,会话ID)是进程会话的标识符。会话是一个或多个进程组的集合,这些进程组共享一个控制终端。会话ID是与会话关联的唯一标识符,通常用于区分不同的会话。每个会话都有一个领头进程(session leader),这个进程通常是创建会话的进程。领头进程的进程ID(PID)通常也是会话ID(SID)。会话领头进程负责管理会话中的进程组,并控制它们对终端的访问。
- setsid是一个Unix命令,用于创建一个新的会话,并使调用进程成为会话的领头进程。调用setsid的进程会成为新会话的唯一进程组,并且没有控制终端。这通常用于编写守护进程(daemon),以确保守护进程不是任何终端的前台进程,从而避免终端关闭时守护进程也被终止。
4.5/dev/null
在Unix和Linux操作系统中,/dev/null 是一个特殊的设备文件,通常被称为空设备或空文件。它有两个主要的特性:
写入 /dev/null 的数据会被丢弃:当你将任何数据写入 /dev/null 时,这些数据都会被操作系统立即丢弃,就像它们被扔进了一个黑洞一样。这个特性使得 /dev/null 成为了一个方便的数据丢弃点。例如,如果你有一个命令的输出你不关心,你可以将其重定向到 /dev/null 以避免在终端上显示它。
bash
some_command > /dev/null
这条命令会执行 some_command,但是任何输出到标准输出的内容都会被丢弃,不会在终端上显示。
从 /dev/null 读取数据会立即返回文件结束:如果你尝试从 /dev/null 读取数据,操作系统会立即返回文件结束(EOF)标志,就像文件已经读取完毕一样。这意味着从 /dev/null 读取不会得到任何数据。
这两个特性使得 /dev/null 在Unix和Linux脚本编写和系统管理中非常有用。以下是一些常见的使用场景:
丢弃不需要的输出:当你运行一个命令,但不想看到其输出时,可以将输出重定向到 /dev/null。
bash
find / -name “*.log” > /dev/null
这条命令会查找系统中所有的 .log 文件,但不会显示任何输出。
忽略输入:当你编写一个程序或脚本,并且你不关心从某个文件或设备读取的输入时,可以将输入重定向自 /dev/null。
bash
some_command < /dev/null
这条命令会执行 some_command,并忽略任何尝试从标准输入读取的数据。
从技术和实现的角度来看,/dev/null 是一个由内核提供的特殊设备,它实现了上述的读写行为。在文件系统层次结构中,它通常位于 /dev 目录下,这个目录包含了所有设备文件,它们代表了系统的各种硬件设备和特殊文件。
4.6守护进程和孤儿进程
守护进程(Daemon)和孤儿进程(Orphan Process)在操作系统中各自扮演着不同的角色,它们之间存在明显的区别。
守护进程是在计算机操作系统中以后台方式运行的长期进程。它们通常在系统启动时启动,并在系统运行期间持续运行,负责执行一些特定的任务或提供某种服务。守护进程的设计理念是为了在系统启动后,持续提供服务或执行任务,而不需要交互式用户干预。守护进程具有后台运行、长期运行和无人值守的特点。它们是特意产生的,用于完成周期性的任务,如Web服务器、数据库服务等。
而孤儿进程则是指其父进程执行完或者被终止后仍然继续运行的进程。孤儿进程的一个主要特征是,它们的父进程ID(PPID)变为1号进程(通常是init进程或其后续版本,如systemd)。孤儿进程是意外产生的,可能由于父进程异常退出或结束而留下。它们通常不占据前台终端,并在后台运行。需要注意的是,虽然孤儿进程在技术上可能是一种资源泄漏,但通常情况下它们并不会对系统造成危害,因为系统会负责清理这些进程。
综上所述,守护进程和孤儿进程的主要区别在于它们的产生方式、运行目的以及在系统中的角色。守护进程是特意为提供服务或执行任务而创建的,而孤儿进程则是由于父进程退出而意外留下的。尽管它们在后台运行且不与用户交互,但它们的存在和行为在操作系统中有不同的意义和处理方式。
linux下自己编写守护进程需要更改当前调用进程的工作目录吗?
在Linux下编写守护进程时,更改当前调用进程的工作目录(也称为chdir)是一个常见的做法,但并不是必须的。这主要取决于你的具体需求和设计考虑。
更改工作目录的主要原因有以下几点:
安全性:守护进程通常不需要访问其启动目录中的文件。将工作目录更改为一个无关紧要的目录(如“/”或“/tmp”)可以减少潜在的安全风险,因为攻击者可能试图利用该目录中的文件或符号链接进行攻击。
资源清理:如果守护进程在其工作目录中创建了临时文件或套接字,更改工作目录可以防止这些资源在守护进程退出后仍然占用文件系统空间。
独立性:更改工作目录有助于确保守护进程与其启动环境完全分离,从而增强其独立性和稳定性。
然而,也有一些情况下你可能不需要更改工作目录:
依赖特定路径:如果守护进程需要访问特定路径下的文件或资源,那么更改工作目录可能会导致这些资源无法访问。
简单性:对于简单的守护进程,不更改工作目录可能更简单、更易于理解和维护。
总的来说,更改工作目录是一个好的做法,但具体是否需要这样做取决于你的应用场景和需求。如果你决定更改工作目录,通常建议将其更改为一个无关紧要的目录,如“/”或“/tmp”。同时,确保在更改工作目录之前已经完成了所有必要的文件打开和套接字创建操作。
5.JSON
JSON(JavaScript Object Notation,JS对象简谱)是一种轻量级的数据交换格式。
它基于ECMAScript的一个子集,采用完全独立于语言的文本格式来存储和表示数据。简单、清晰的层次结构使得JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
JSON建构于两种结构:
“名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组(associative array)。
值的有序列表(An ordered list of values)。在多数语言中,它被理解为数组(array)。
这些结构可以被嵌套,例如,对象里可以包含数组,数组里可以包含对象,对象里还可以包含另一个对象,等等。
总的来说,JSON是一种非常灵活且易于使用的数据格式,它广泛应用于网络数据传输和配置文件等场景。
6.完整代码
6.1Makefile
.PHONY:all
all:client CalServerclient:CalClient.ccg++ -o $@ $^ -std=c++11 -ljsoncpp -DDEBUG_COMPILE
CalServer:CalServer.ccg++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread -DDEBUG_COMPILE.PHONY:clean
clean:rm -f client CalServer
6.2Socket.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
public:Sock() {}int Socket(){int socketFd = socket(AF_INET, SOCK_STREAM, 0);if (socketFd < 0){logMsg(FATAL, "create socket error, %d:%s", errno, strerror(errno));exit(2);}logMsg(NORMAL, "create socket success, listenSocket: %d", socketFd);return socketFd;}void Bind(int socket, uint16_t port, std::string ip = "0.0.0.0"){struct sockaddr_in local;memset(&local, 0, sizeof local);local.sin_family = AF_INET;local.sin_port = htons(port);if (bind(socket, (struct sockaddr *)&local, sizeof(local)) < 0){logMsg(FATAL, "bind error, %d:%s", errno, strerror(errno));exit(3);}}void Listen(int socket){if (listen(socket, g_backlog) < 0){logMsg(FATAL, "listen error, %d:%s", errno, strerror(errno));exit(4);}logMsg(NORMAL, "listen success");}// const std::string&: 输入型参数// std::string*: 输出型参数// std::string&: 输入输出型参数//服务端接受客户端发来的连接请求 并 把来源客户端的信息存入对应字段int Accept(int listenSocket, std::string *clientIp, uint16_t *clientPort){struct sockaddr_in src;socklen_t len = sizeof(src);int serviceSocket = accept(listenSocket, (struct sockaddr *)&src, &len);if (serviceSocket < 0){logMsg(ERROR, "accept error, %d:%s", errno, strerror(errno));return -1;}if (clientPort != nullptr)*clientPort = ntohs(src.sin_port);if (clientIp != nullptr)*clientIp = inet_ntoa(src.sin_addr);return serviceSocket;}//客户端向服务端发起连接请求bool Connect(int socketFd, const std::string &serverIp, const uint16_t &serverPort){struct sockaddr_in svr_sockAddr;memset(&svr_sockAddr, 0, sizeof(svr_sockAddr));svr_sockAddr.sin_family = AF_INET;svr_sockAddr.sin_addr.s_addr = inet_addr(serverIp.c_str());svr_sockAddr.sin_port = htons(serverPort);if (connect(socketFd, (struct sockaddr *)&svr_sockAddr, sizeof(svr_sockAddr)) == 0)return true;elsereturn false;}~Sock() {}private:static const int g_backlog = 20;
};
6.3Protocol.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <jsoncpp/json/json.h>namespace Protocol
{
#define MYSELF 0#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)#define DIVZERO 1
#define MODZERO 2
#define ILLEGAL 3class Request{public:Request(){}Request(int left, int right, char op): _left(left),_right(right),_op(op){}~Request() {}std::string Serialize(){
#ifdef MYSELF//"left _op _right"std::string str;str = std::to_string(_left);str += SPACE;str += _op;str += SPACE;str += std::to_string(_right);return str;
#else//创建并序列化一个JSON对象Json::Value root;root["left"] = _left;root["right"] = _right;root["op"] = _op;Json::FastWriter writer;return writer.write(root);
#endif}bool Deserialize(const std::string &str){
#ifdef MYSELF//"left _op _right"std::size_t firstSpace = str.find(SPACE);if (firstSpace == std::string::npos)return false;std::size_t right = str.rfind(SPACE);if (right == std::string::npos)return false;_left = atoi(str.substr(0, firstSpace).c_str());_right = atoi(str.substr(right + SPACE_LEN).c_str());if (firstSpace + SPACE_LEN > str.size())return false;else_op = str[firstSpace + SPACE_LEN];return true;
#else//解析一个JSON字符串并提取值Json::Value root;Json::Reader reader;reader.parse(str, root);_left = root["left"].asInt();_right = root["right"].asInt();_op = root["op"].asInt();return true;
#endif}public:int _left;int _right;char _op;};class Response{public:Response(){}Response(int result, int code, int left, int right, char op): _result(result),_code(code),_left(left),_right(right),_op(op){}~Response() {}// "_code result_"std::string Serialize(){
#ifdef MYSELFstd::string s;s = std::to_string(_code);s += SPACE;s += std::to_string(_result);return s;
#elseJson::Value root;root["code"] = _code;root["result"] = _result;root["xx"] = _left;root["yy"] = _right;root["zz"] = _op;Json::FastWriter writer;return writer.write(root);
#endif}// "_code result_"bool Deserialize(const std::string &s){
#ifdef MYSELFstd::size_t pos = s.find(SPACE);if (pos == std::string::npos)return false;_code = atoi(s.substr(0, pos).c_str());_result = atoi(s.substr(pos + SPACE_LEN).c_str());return true;
#elseJson::Value root;Json::Reader reader;reader.parse(s, root);_code = root["code"].asInt();_result = root["result"].asInt();_left = root["xx"].asInt();_right = root["yy"].asInt();_op = root["zz"].asInt();return true;
#endif}public:int _result;int _code;//一下成员变量实际上不用 此处为了测试JSON比自定义协议更容易扩展int _left;int _right;char _op;};bool Recv(int socket, std::string *out){// UDP是面向数据报, TCP 面向字节流的// recv不能保证读到的inbuffer是一个完整的请求 对协议进一步定制char buffer[1024];ssize_t s = recv(socket, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;*out += buffer;}else if (s == 0){// std::cout << "client quit" << std::endl;return false;}else{// std::cout << "recv error" << std::endl;return false;}return true;}void Send(int socket, const std::string str){// std::cout << "sent in" << std::endl;int sendBytes = send(socket, str.c_str(), str.size(), 0);// if (sendBytes < 0)// std::cout << "send error" << std::endl;}// "length\r\nXXXXXX\r\n"std::string Encode(std::string &s){std::string new_package = std::to_string(s.size());new_package += SEP;new_package += s;new_package += SEP;return new_package;}// "length\r\n_left _op _right\r\n"std::string Decode(std::string &buffer){std::size_t pos = buffer.find(SEP);if (pos == std::string::npos)return "";int length = atoi(buffer.substr(0, pos).c_str());int surplus = buffer.size() - pos - 2 * SEP_LEN;if (surplus < length)return "";// 至少具有一个完整的报文buffer.erase(0, pos + SEP_LEN);std::string msg = buffer.substr(0, length);buffer.erase(0, length + SEP_LEN);return msg;}
}
6.4Log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *gLevelMap[] = {" DEBUG "," NORMAL","WARNING"," ERROR "," FATAL "};#define LOGFILE "./Calculator.log"// 日志功能: 日志等级 时间 用户自定义(日志内容/文件名/文件行) 等
void logMsg(int level, const char *format, ...)
{
#ifndef DEBUG_COMPILE // 非调试编译下 不输出DEBUG信息if (level == DEBUG)return;
#endif// 1.标准日志内容char stdBuf[1024];// 1.1获取时间戳time_t timestamp = time(nullptr);if (timestamp == std::time_t(-1)){std::cerr << "获取时间失败" << std::endl;exit(1);}// 1.2获取格式化时间struct tm *CLK = std::localtime(×tamp); // tm *localtime(const time_t *__timer)// 1.3将日志信息输出到日志文件// snprintf(stdBuf, sizeof stdBuf, "[%s] [%ld] ", gLevelMap[level], timestamp);snprintf(stdBuf, sizeof stdBuf, "[%s] [%d/%d/%d %d:%d:%d ", gLevelMap[level],1900 + CLK->tm_year, 1 + CLK->tm_mon, CLK->tm_mday, CLK->tm_hour, CLK->tm_min, CLK->tm_sec);// 2.用户自定义内容va_list args;va_start(args, format);char logBuf[1024];// int vsnprintf(char *str, size_t size, const char *format, va_list ap);vsnprintf(logBuf, sizeof logBuf, format, args);va_end(args);//fprintf(stdout, "%s%s\n", stdBuf, logBuf);FILE *fp = fopen(LOGFILE, "a");fprintf(fp, "%s%s\n", stdBuf, logBuf);fclose(fp);
}
6.5Daemon.hpp
#pragma once#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void MyDaemon()
{// 1. 忽略信号signal(SIGPIPE, SIG_IGN);signal(SIGCHLD, SIG_IGN);// 2. 不是进程组-组长的进程才能成功调用setsidpid_t id = fork();assert(id >= 0);if (id > 0)exit(0);if (0 == id){// 3. 调用setsidsetsid();// 4. 重定向:守护进程不能直接向显示器打印消息int dev_null = open("/dev/null", O_RDONLY | O_WRONLY);if (dev_null > 0){dup2(0, dev_null);dup2(1, dev_null);dup2(2, dev_null);close(dev_null);}}
}
6.6TcpServer.hpp
#pragma once#include <vector>
#include <functional>
#include <pthread.h>
#include "Socket.hpp"namespace Tcpserver
{using func_t = std::function<void(int)>;class TcpServer;class ThreadInfo{public:ThreadInfo(int socket, TcpServer *server): _socket(socket),_ptrServer(server){}~ThreadInfo() {}public:int _socket;TcpServer *_ptrServer;};class TcpServer{private:static void *ThreadRoutine(void *args){pthread_detach(pthread_self());ThreadInfo *threadinfo = static_cast<ThreadInfo *>(args);threadinfo->_ptrServer->Excute(threadinfo->_socket);close(threadinfo->_socket);delete threadinfo;return nullptr;}public:TcpServer(const uint16_t &port, const std::string &ip = "0.0.0.0"){_listenSocket = _socket.Socket();_socket.Bind(_listenSocket, port, ip);_socket.Listen(_listenSocket);}void Start(){while (true){std::string clientIp;uint16_t clientPort;int serviceSocket = _socket.Accept(_listenSocket, &clientIp, &clientPort);if (serviceSocket == -1)continue;logMsg(NORMAL, "create new link success, sock: %d", serviceSocket);pthread_t tid;ThreadInfo *threadinfo = new ThreadInfo(serviceSocket, this);pthread_create(&tid, nullptr, ThreadRoutine, threadinfo);}}void AddService(func_t func){_func.push_back(func);}void Excute(int socket){for (auto &fun : _func){fun(socket);}}~TcpServer(){if (_listenSocket >= 0)close(_listenSocket);}private:int _listenSocket;Sock _socket;std::vector<func_t> _func;};
}
6.7Client.cc
#include <iostream>
#include "Socket.hpp"
#include "Protocol.hpp"using namespace Protocol;static void usage(const std::string &process)
{std::cout << std::endl<< "Usage: " << process << " serverIp serverPort" << std::endl<< std::endl;
}// ./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]);Sock sock;int socketFd = sock.Socket();if (!sock.Connect(socketFd, serverIp, serverPort)){std::cerr << "Connect error" << std::endl;exit(2);}bool quit = false;std::string buffer;// 客户端和服务端默认遵守协议:输入的就是"1 + 1"while (!quit){// 1. 获取需求Request req;std::cout << "Please Enter # ";std::cin >> req._left >> req._op >> req._right;// 2. 序列化std::string str = req.Serialize();// 3. 添加长度报头str = Encode(str);// 4. 发送给服务端Send(socketFd, str);// 5. 读取结果while (true){bool recvOk = Recv(socketFd, &buffer);if (!recvOk){quit = true;break;}std::string package = Decode(buffer);if (package.empty())continue;Response resp;resp.Deserialize(package);std::string err;switch (resp._code){case DIVZERO:err = "除0错误";break;case MODZERO:err = "模0错误";break;case ILLEGAL:err = "非法操作";break;default:// 该输出只在JSON协议下有用 因为自定义协议的response没有对输出信息做反序列化std::cout << resp._left << resp._op << resp._right << " = " << resp._result << " [success]" << std::endl;break;}if (!err.empty())std::cerr << err << std::endl;break;}}close(socketFd);return 0;
}
6.8Server.cc
#include "TcpServer.hpp"
#include "Protocol.hpp"
#include "Daemon.hpp"
#include <memory>
#include <signal.h>using namespace Tcpserver;
using namespace Protocol;static void usage(const std::string &process)
{std::cout << std::endl<< "Usage: " << process << " port" << std::endl<< std::endl;
}static Response calculate(const Request &req)
{Response resp(0, 0, req._left, req._right, req._op);switch (req._op){case '+':resp._result = req._left + req._right;break;case '-':resp._result = req._left - req._right;break;case '*':resp._result = req._left * req._right;break;case '/':if (0 == req._right)resp._code = DIVZERO;elseresp._result = req._left / req._right;break;case '%':if (0 == req._right)resp._code = MODZERO;elseresp._result = req._left % req._right;break;default:resp._code = ILLEGAL;break;}return resp;
}void calculateService(int serviceSocket)
{std::string inbuffer;while (true){// 1. 读取客户端发的信息bool recvOk = Recv(serviceSocket, &inbuffer);if (!recvOk)break;// 2. 协议解析 得到一个完整的报文std::string package = Decode(inbuffer);if (package.empty())continue;logMsg(NORMAL, "package: %s", package.c_str());// 3. 反序列化 字节流 -> 结构化Request req;req.Deserialize(package);// 5. 业务逻辑Response resp = calculate(req);// 6. 对计算结果进行序列化std::string respString = resp.Serialize(); // 7. 添加长度信息 "length\r\ncode result\r\n"respString = Encode(respString);Send(serviceSocket, respString);}
}// ./CalServer port
int main(int argc, char *argv[])
{if (argc != 2){usage(argv[0]);exit(1);}// server的编写要有较为严谨的判断逻辑// 一般服务器都要忽略SIGPIPE信号 防止在运行中出现非法写入导致服务端终止的问题// signal(SIGPIPE, SIG_IGN);//MyDaemon();std::unique_ptr<TcpServer> server(new TcpServer(atoi(argv[1])));server->AddService(calculateService);server->Start();return 0;
}