从零开发一个WebSocket服务器

从零开发一个WebSocket服务器 WebSocket 协议是为了解决 http 协议的无状态、短连接(通常是)和服务端无法主动给客户端推送数据等问题而开发的新型协议,其通信基础也是基于 TCP。由于较旧的浏览器可能不支持 WebSocket 协议,所以使用 WebSocket 协议的通信双方在进行 TCP 三次握手之后,还要再额外地进行一次握手,这一次的握手通信双方的报文格式是基于 HTTP 协议改造的。 WebSocket 握手过程 TCP 三次握手的过程我们就不在这里赘述了,任何一本网络通信书籍上都有详细的介绍。我们这里来介绍一下 WebSocket 通信最后一次的握手过程。 握手开始后,一方给另外一方发送一个 http 协议格式的报文,这个报文格式大致如下: GET /realtime HTTP/1.1\r\n Host: 127.0.0.1:9989\r\n Connection: Upgrade\r\n Pragma: no-cache\r\n Cache-Control: no-cache\r\n User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n Upgrade: websocket\r\n Origin: http://xyz.com\r\n Sec-WebSocket-Version: 13\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n Sec-WebSocket-Key: IqcAWodjyPDJuhGgZwkpKg==\r\n Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n \r\n 对这个格式有如下要求: 握手必须是一个有效的 HTTP 请求; 请求的方法必须为 GET,且 HTTP 版本必须是 1.1; 请求必须包含 Host 字段信息; 请求必须包含 Upgrade字段信息,值必须为 websocket; 请求必须包含 Connection 字段信息,值必须为 Upgrade; 请求必须包含 Sec-WebSocket-Key 字段,该字段值是客户端的标识编码成 base64 格式; 请求必须包含 Sec-WebSocket-Version 字段信息,值必须为 13; 请求必须包含 Origin 字段; 请求可能包含 Sec-WebSocket-Protocol 字段,规定子协议; 请求可能包含 Sec-WebSocket-Extensions字段规定协议扩展; 请求可能包含其他字段,如 cookie 等。 对端收到该数据包后如果支持 WebSocket 协议,会回复一个 http 格式的应答,这个应答报文的格式大致如下: ...

2021-01-11

从零实现一个http服务器

从零实现一个http服务器 我始终觉得,天生的出身很重要,但后天的努力更加重要,所以如今的很多“科班”往往不如后天努力的“非科班”。所以,我们需要重新给“专业”和“专家”下一个定义:所谓专业,就是别人不搞你搞,这就是你的“专业”;你和别人同时搞,你比别人搞的好,就是“专家”。 说到http协议和http请求,很多人都知道,但是他们真的“知道”吗?我面试过很多求职者,一说到http协议,他们能滔滔不绝,然后我问他http协议的具体格式是啥样子的?很多人不清楚,不清楚就不清楚吧,他甚至能将http协议的头扯到html文档头部。当我问http GET和POST请求的时候,GET请求是什么形式一般人都可以答出来,但是POST请求的数据放在哪里,服务器如何识别和解析这些POST数据,很多人又说不清道不明了。当说到http服务器时,很多人离开了apache、Nginx这样现成的http server之外,自己实现一个http服务器无从下手,如果实际应用场景有需要使用到一些简单http请求时,使用apache、Nginx这样重量级的http服务器程序实在劳师动众,你可以尝试自己实现一个简单的。 上面提到的问题,如果您不能清晰地回答出来,可以阅读一下这篇文章,这篇文章在不仅介绍http的格式,同时带领大家从零实现一个简单的http服务器程序。 一、项目背景 最近很多朋友希望我的flamingo服务器支持http协议,我自己也想做一个微信小程序,小程序通过http协议连接通过我的flamingo服务器进行聊天。flamingo是一个开源的即时通讯软件,目前除了服务器端,还有pc端、android端,后面会支持更多的终端。关于flamingo的介绍您可以参考这里: https://github.com/baloonwj/flamingo,更新日志:https://github.com/baloonwj/flamingo/issues/1。下面是flamingo的部分截图: 二、http协议介绍 1. http协议是应用层协议,一般建立在tcp协议的基础之上(当然你的实现非要基于udp也是可以的),也就是说http协议的数据收发是通过tcp协议的。 2. http协议也分为head和body两部分,但是我们一般说的html中的和标记不是http协议的头和身体,它们都是http协议的body部分。 那么http协议的头到底长啥样子呢?我们来介绍一下http协议吧。 http协议的格式如下: GET或POST 请求的url路径(一般是去掉域名的路径) HTTP协议版本号\r\n 字段1名: 字段1值\r\n 字段2名: 字段2值\r\n … 字段n名 : 字段n值\r\n \r\n http协议包体内容 也就是说http协议由两部分组成:包头和包体,包头与包体之间使用一个\r\n分割,由于http协议包头的每一行都是以**\r\n结束,所以http协议包头一般以\r\n\r\n**结束。 举个例子,比如我们在浏览器中请求http://www.hootina.org/index_2013.php这个网址,这是一个典型的GET方法,浏览器组装的http数据包格式如下: GET /index_2013.php HTTP/1.1\r\n Host: www.hootina.org\r\n Connection: keep-alive\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n Accept-Encoding: gzip, deflate\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n \r\n 上面这个请求只有包头没有包体,http协议的包体不是必须的,也就是说GET请求一般没有包体。 如果GET请求带参数,那么一般是附加在请求的url后面,参数与参数之间使用&分割,例如请求http://www.hootina.org/index_2013.php?param1=value1&param2=value2&param3=value3,我们看下这个请求组装的的http协议包格式: GET /index_2013.php?param1=value1&param2=value2&param3=value3 HTTP/1.1\r\n Host: www.hootina.org\r\n Connection: keep-alive\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n Accept-Encoding: gzip, deflate\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n \r\n 对比一下,你现在知道http协议的GET参数放在协议包的什么位置了吧。 那么POST的数据放在什么位置呢?我们再12306网站https://kyfw.12306.cn/otn/login/init中登陆输入用户名和密码: 然后发现浏览器以POST方式组装了http协议包发送了我们的用户名、密码和其他一些信息,组装的包格式如下: POST /passport/web/login HTTP/1.1\r\n Host: kyfw.12306.cn\r\n Connection: keep-alive\r\n Content-Length: 55\r\n Accept: application/json, text/javascript, */*; q=0.01\r\n Origin: https://kyfw.12306.cn\r\n X-Requested-With: XMLHttpRequest\r\n User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36\r\n Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n Referer: https://kyfw.12306.cn/otn/login/init\r\n Accept-Encoding: gzip, deflate, br\r\n Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n Cookie: _passport_session=0b2cc5b86eb74bcc976bfa9dfef3e8a20712; _passport_ct=18d19b0930954d76b8057c732ce4cdcat8137; route=6f50b51faa11b987e576cdb301e545c4; RAIL_EXPIRATION=1526718782244; RAIL_DEVICEID=QuRAhOyIWv9lwWEhkq03x5Yl_livKZxx7gW6_-52oTZQda1c4zmVWxdw5Zk79xSDFHe9LJ57F8luYOFp_yahxDXQAOmEV8U1VgXavacuM2UPCFy3knfn42yTsJM3EYOy-hwpsP-jTb2OXevJj5acf40XsvsPDcM7; BIGipServerpool_passport=300745226.50215.0000; BIGipServerotn=1257243146.38945.0000; BIGipServerpassport=1005060362.50215.0000\r\n \r\n username=balloonwj%40qq.com&password=iloveyou&appid=otn 其中username=balloonwj%40qq.com&password=iloveyou&appid=otn就是我们的POST数据,但是大家需要注意的以下几种,不要搞错: **1. 我的用户名是balloonwj@qq.com,到POST里面变成balloonwj%40qq.com,其中%40是@符号的16进制转码形式。这个码表可以参考这里:**http://www.w3school.com.cn/tags/html_ref_urlencode.html ...

2021-01-11

从零实现一个邮件收发客户端

从零实现一个邮件收发客户端 与邮件收发有关的协议有 POP3、SMPT 和 IMAP 等。 POP3 POP3全称是 Post Office Protocol 3 ,即邮局协议的第 3 个版本,它规定怎样将个人计算机连接到 Internet 的邮件服务器和下载电子邮件的电子协议,它是因特网电子邮件的第一个离线协议标准,POP3 允许用户从服务器上把邮件存储到本地主机(即自己的计算机)上,同时删除保存在邮件服务器上的邮件,而 POP3 服务器则是遵循 POP3 协议的接收邮件服务器,用来接收电子邮件的。 SMTP SMTP 的全称是 Simple Mail Transfer Protocol,即简单邮件传输协议。它是一组用于从源地址到目的地址传输邮件的规范,它帮助每台计算机在发送或中转邮件时找到下一个目的地。SMTP 服务器就是遵循 SMTP 协议的发送邮件服务器。SMTP 需要认证,简单地说就是要求必须在提供了账户名和密码之后才可以登录 SMTP 服务器,这就使得那些垃圾邮件的散播者无可乘之机,使用户避免受到垃圾邮件的侵扰。 IMAP IMAP全称是 Internet Mail Access Protocol,即交互式邮件存取协议,它是跟 POP3 类似邮件访问标准协议之一。不同的是,开启了 IMAP 后,在电子邮件客户端收取的邮件仍然保留在服务器上,同时在客户端上的操作都会反馈到服务器上,如:删除邮件,标记已读等,服务器上的邮件也会做相应的动作。所以无论从浏览器登录邮箱或者客户端软件登录邮箱,看到的邮件以及状态都是一致的。而 POP3 对邮件的操作只会在本地邮件客户端起作用。 读者如果需要自己编写相关的邮件收发客户端,需要登录对应的邮件服务器开启相应的 POP3/SMTP/IMAP 服务。以 163 邮箱为例: 请登录 163 邮箱(http://mail.163.com/),点击页面正上方的“设置”,再点击左侧上“POP3/SMTP/IMAP”,其中“开启 SMTP 服务”是系统默认勾选开启的。读者可勾选图中另两个选项,点击确定,即可开启成功。不勾选图中两个选项,点击确定,可关闭成功。 网易163免费邮箱相关服务器信息: 163免费邮客户端设置的POP3、SMTP、IMAP地址 POP3、SMTP、IMAP 协议就是我们前面介绍的以指定字符(串)为包的结束标志的协议典型例子。我们来以 SMTP 协议和 POP3 协议为例来讲解一下。 SMTP 协议 先来介绍 SMTP 协议吧,SMTP 全称是 Simple Mail Transfer Protocol,即简单邮件传输协议,该协议用于发送邮件。 SMTP 协议的格式: 关键字 自定义内容\r\n “自定义内容”根据“关键字”的类型是否设置,对于使用 SMTP 作为客户端的一方常用的“关键字“如下所示: //连接上邮件服务器之后登录服务器之前向服务器发送的问候信息 HELO 自定义问候语\r\n //请求登录邮件服务器 AUTH LOGIN\r\n base64形式的用户名\r\n base64形式的密码\r\n //设置发件人邮箱地址 MAIL FROM:发件人地址\r\n //设置收件人地址,每次发送可设置一个收件人地址,如果有多个收件地址,要分别设置对应次数 rcpt to:收件人地址\r\n //发送邮件正文开始标志 DATA\r\n //发送邮件正文,注意邮件正文以.\r\n结束 邮件正文\r\n.\r\n //登出服务器 QUIT\r\n 使用 SMTP 作为邮件服务器的一方常用的“关键字“是定义的各种应答码,应答码后面可以带上自己的信息,然后以\r\n作为结束,格式如下: ...

2021-01-11

从零实现一款12306刷票软件

从零实现一款12306刷票软件 写在前面的话 每年逢年过节,一票难求读者肯定不陌生。这篇文章,我们带领读者从零实现一款12306刷票软件,其核心原理是通过发送http请求模拟登录12306网站的购票的过程,最后买到票。 关于http请求的格式和如何组装http数据包给服务器发送请求,我们在上一篇文章《从零实现一个http服务器》中已经详细介绍过了,如果还不明白的朋友可以去那篇文章看下。 郑重申明一下:这里介绍的技术仅供用于学习,不可用于恶意攻击12306服务器,请勿滥用本文介绍的技术。对12306服务器造成的任何损失,后果自负。 当然,由于12306服务器用户量巨大,为了防止黄牛和其他一些非法攻击者,12306的很多url和在购票过程中各个步骤的协议细节经常发生变化。所以,本文中介绍的一些具体的url,可能在你看到本文时已经失效。但是这并没有关系,只要你掌握了本文中介绍的分析方法,您就可以灵活地修改您的代码,以适应最新的12306服务器的要求。 举个例子,如12306的查票接口目前的url是: https://kyfw.12306.cn/otn/leftTicket/query 可能过几天就变成了 https://kyfw.12306.cn/otn/leftTicket/queryX 再过几天又可能变成 https://kyfw.12306.cn/otn/leftTicket/queryY 然后一个星期后又可能变成 https://kyfw.12306.cn/otn/leftTicket/queryZ 这些笔者都见过。所以,重在原理的学习,掌握了原理,不管12306的相关url变成什么样,都可以以不变应万变。哎,12306在与黄牛的斗争中越走越远啊T_T 本文将使用以下工具来分析12306购票的过程,然后使用C++语言,模拟相关的过程,最终购票。 Chrome浏览器(其他的浏览器也可以,都有类似的界面,如Chrome,装了httpwatch的IE浏览器等) 一个可以登录12306网址并且可以购票的12306账号 Visual Studio(版本随意,我这里用的是VS 2013) 一、查票与站点信息接口 之所以先分析这个接口,是因为查票不需要用户登录的,相对来说最简单。我们在Chrome浏览器中打开12306余票查询页面,网址是:https://kyfw.12306.cn/otn/leftTicket/init,如下图所示: 然后在页面中右键菜单中选择【检查】菜单,打开后,选择【网络】选项卡。如下图所示: 打开后页面变成二分窗口了,左侧是正常的网页页面,右侧是浏览器自带的控制台,当我们在左侧页面中进行操作后,右侧会显示我们浏览器发送的各种http请求和应答。我们这里随便查一个票吧,如查2018年5月20日从上海到北京的票,点击12306网页中【查询】按钮后,我们发现右侧是这样的: 通过图中列表的type值是xhr,我们可以得出这是一个ajax请求(ajax是一种浏览器原生支持的异步技术,具体细节请读者自行搜索)。我们选择这个请求,你能看到这个请求的细节——请求和响应结果: 在reponse中,我们可以看到我们的这个http的去除http头的响应结果(包体,可能是解压缩或者解码后的): 这是一个json格式,我们找个json格式化工具,把这个json格式化后贴出来给大家看一下,其实您后面会发现12306的http请求结果中与购票相关的数据基本上都是json格式。这里的json如下: { "validateMessagesShowId": "_validatorMessage", "status": true, "httpstatus": 200, "data": { "result": ["null|23:00-06:00系统维护时间|5l0000G10270|G102|AOH|VNP|AOH|VNP|06:26|12:29|06:03|IS_TIME_NOT_BUY|RLVVIt093U2EZuy2NE+VQyRloXyqTzFp6YyNk6J52QcHEA01|20180520|3|HZ|01|11|1|0|||||||||||1|有|13||O090M0|O9M|0",(内容太长,这里省略) "], "flag": "1", "map": { "AOH": "上海虹桥", "BJP": "北京", "VNP": "北京南", "SHH": "上海" } }, "messages": [], "validateMessages": {} } 其中含有的余票信息在result节点中,这是一个数组。每个节点以|分割,我们可以格式化后显示在自己的界面上: 我这里做的界面比较简陋,读者如果有兴趣可以做更精美的界面。我们列下这个请求发送的http数据包和应答包: 请求包: GET /otn/leftTicket/query?leftTicketDTO.train_date=2018-05-20&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=BJP&purpose_codes=ADULT HTTP/1.1 Host: kyfw.12306.cn Connection: keep-alive Cache-Control: no-cache Accept: */* X-Requested-With: XMLHttpRequest If-Modified-Since: 0 User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36 Referer: https://kyfw.12306.cn/otn/leftTicket/init Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: RAIL_EXPIRATION=1526978933395; RAIL_DEVICEID=WKxIYg-q1zjIPVu7VjulZ9PqEGvW2gUB9LvoM1Vx8fa7l3SUwnO_BVSatbTq506c6VYNOaxAiRaUcGFTMjCz9cPayEIc9vJ0pHaXdSqDlujJP8YrIoXbpAAs60l99z8bEtnHgAJzxLzKiv2nka5nmLY_BMNur8b8; _jc_save_fromStation=%u4E0A%u6D77%2CSHH; _jc_save_toStation=%u5317%u4EAC%2CBJP; _jc_save_fromDate=2018-05-20; _jc_save_toDate=2018-05-19; _jc_save_wfdc_flag=dc 应答包: ...

2021-01-11

从零学习开源项目系列(二) 最后一战概况

从零学习开源项目系列(二) 最后一战概况 这份代码我也是无意中来自一个朋友,据他说也是来源于互联网,服务器端代码原来是Linux版本的,但被厉害的大神修改成可以在Windows上运行。(如果不小心侵犯了您的版权,请联系我删除)。好在,这份代码中使用的大多数方法和接口都是可以跨Windows和Linux两个平台的,所以Linux开发下的朋友请不要感到不适,我们学习这份代码更多的不是纠结细节而是学习思路和原理。 游戏主solution文件用Visual Studio打开后如下图所示: 这里总共有10个工程项目,模块比较多。**我们应该从何处入手呢?**我们先看下源码目录: 我们进入Server目录,发现如下一个文件: 我们打开看一下内容: cd Bin\x64\Release start.bat 这个代码进入Bin\x64\Release目录,执行另外一个start.bat,我们进入这个目录去看下这个文件内容: taskkill /f /t /im redis-server.exe taskkill /f /t /im CSBattleMgr.exe taskkill /f /t /im SSBattleMgr.exe taskkill /f /t /im GSConsole.exe taskkill /f /t /im BalanceServer.exe taskkill /f /t /im LoginServer.exe taskkill /f /t /im GSKernel.exe taskkill /f /t /im RobotConsole.exe taskkill /f /t /im LogServer.exe ping -n 1 127.0>nul start /min "redis-server" "redis-server.exe" redis.conf ping -n 1 127.0>nul start /min "redis-Logicserver" "redis-server.exe" redis-logic.conf ping -n 1 127.0>nul echo "start CSBattleMgr.exe" start /min "CSBattleMgr" "CSBattleMgr.exe" ping -n 1 127.0>nul echo "start SSBattleMgr.exe" start /min "SSBattleMgr" "SSBattleMgr.exe" ping -n 1 127.0>nul echo "start GSKernel.exe" start /min "GSKernel" "GSKernel.exe" ping -n 1 127.0>nul echo "start BalanceServer.exe" start /min "BalanceServer" "BalanceServer.exe" ping -n 1 127.0>nul echo "start LoginServer.exe" start /min "LoginServer" "LoginServer.exe" ping -n 1 127.0>nul echo "start LogServer.exe" start /min "LogServer" "LogServer.exe" 通过这个脚本,我们得到了这个服务器项目的一些信息,这个服务器由以下一些服务组成: ...

2021-01-11

从零学习开源项目系列(三) CSBattleMgr服务源码研究

从零学习开源项目系列(三) CSBattleMgr服务源码研究 服务器项目工程如下图所示: 如上图所示,这篇文章我们将介绍CSBattleMgr的情况,但是我们不会去研究这个服务器的特别细节的东西(这些细节我们将在后面的文章中介绍)。阅读一个未知的项目源码如果我们开始就纠结于各种细节,那么我们最终会陷入“横看成岭侧成峰,远近高低各不同”的尴尬境界,浪费时间不说,可能收获也是事倍功半。所以,尽管我们不熟悉这套代码,我们还是尽量先从整体来把握,先大致了解各个服务的功能,细节部分回头再针对性地去研究。 这个系列的第二篇文章《从零学习开源项目系列(二) 最后一战概况》中我们介绍了,这套游戏的服务需要使用redis和mysql,我们先看下mysql是否准备好了(mysql服务启动起来,数据库建表数据存在,具体细节请参考第二篇文章)。打开Windows的cmd程序,输入以下指令连接mysql: mysql -uroot -p123321 连接成功以后,如下图所示: 然后我们输入以下指令,查看我们需要的数据库是否创建成功: show databases; 这些都是基本的sql语句,如果您不熟悉的话,可能需要专门学习一下。 数据库创建成功后如下图所示: 至于数据库中的表是否创建成功,我们这里先不关注,后面我们实际用到哪张数据表,我们再去研究。 mysql没问题了,接下来我们要启动一下redis,通过第二篇文章我们知道redis需要启动两次,也就是一共两个redis进程,我们游戏服务中分别称为redis-server和redis-login-server(它们的配置文件信息不一样),我们可以在Server\Bin\x64\Release目录下手动cmd命令行执行下列语句: start /min "redis-server" "redis-server.exe" redis.conf start /min "redis-Logicserver" "redis-server.exe" redis-logic.conf 但是这样比较麻烦,我将这两句拷贝出来,放入一个叫start-redis.bat文件中了,每次启动只要执行一下这个bat文件就可以: redis和redis-logic服务启动后如下图所示: 我们常见的redis服务都是linux下的源码,微软公司对redis源码进行了改造,出了一个Windows版本,稍微有点不尽人意(例如:Windows下没有完全与linux的fork()相匹配的API,所以只能用CreateProcess()去替代)。关于windows版本的redis源码官方下载地址为:https://github.com/MicrosoftArchive/redis/releases。 在启动好了mysql和redis后,我们现在正式来看一下CSBattleMgr这个服务。读者不禁可能要问,那么多服务,你怎么知道要先看这个服务呢?我们上一篇文章中也说过,我们再start.bat文件中发现除了redis以外,这是第三个需要启动的服务,所以我们先研究它(start.bat我们可以认为是源码作者为我们留下的部署步骤“文档”): 我们打开CSBattleMgr服务main.cpp文件,找到入口main函数,内容如下: int main(){ DbgLib::CDebugFx::SetExceptionHandler(true); DbgLib::CDebugFx::SetExceptionCallback(ExceptionCallback, NULL); GetCSKernelInstance(); GetCSUserMgrInstance(); GetBattleMgrInstance(); GetCSKernelInstance()->Initialize(); GetBattleMgrInstance()->Initialize(); GetCSUserMgrInstance()->Initialize(); GetCSKernelInstance()->Start(); mysql_library_init(0, NULL, NULL); GetCSKernelInstance()->MainLoop(); } 通过调试,我们发下这个函数大致做了以下任务: //1. 设置程序异常处理函数 //2. 初始化一系列单例对象 //3. 初始化mysql //4. 进入一个被称作“主循环”的无限循环 步骤1设置程序异常处理函数没有好介绍的,我们看一下步骤2初始化一系列单例对象,总共初始化了三个类的对象CCSKernel、CCSUserMgr和CCSBattleMgr。单例模式本身没啥好介绍的,但是有人要提单例模式的线程安全性,所以出现很多通过加锁的单例模式代码,我个人觉得没必要;认为要加锁的朋友可能认为单例对象如果在第一次初始化时同时被多个线程调用就会有问题,我觉得加锁带来的开销还不如像上面的代码一样,在整个程序初始化初期获取一下单例对象,让单例对象生成出来,后面即使多个线程获取这个单例对象也都是读操作,无需加锁。以GetCSKernelInstance();为例: CCSKernel* GetCSKernelInstance(){ return &CCSKernel::GetInstance(); } CCSKernel& CCSKernel::GetInstance(){ if (NULL == pInstance){ pInstance = new CCSKernel; } return *pInstance; } GetCSKernelInstance()->Initialize()的初始化动作其实是加载各种配置信息和事先设置一系列的回调函数和定时器: INT32 CCSKernel::Initialize() { //JJIAZ加载配置的时候 不要随便调整顺序 CCSCfgMgr::getInstance().Initalize(); INT32 n32Init = LoadCfg(); if (eNormal != n32Init) { ELOG(LOG_ERROR," loadCfg()............failed!"); return n32Init; } if(m_sCSKernelCfg.un32MaxSSNum > 0 ) { m_psSSNetInfoList = new SSSNetInfo[m_sCSKernelCfg.un32MaxSSNum]; memset(m_psSSNetInfoList, 0, sizeof(SSSNetInfo) * m_sCSKernelCfg.un32MaxSSNum); m_psGSNetInfoList = new SGSNetInfo[m_sCSKernelCfg.un32MaxGSNum]; memset(m_psGSNetInfoList, 0, sizeof(SGSNetInfo) * m_sCSKernelCfg.un32MaxGSNum); m_psRCNetInfoList = new SRCNetInfo[10]; } m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_AskRegiste] = std::bind(&CCSKernel::OnMsgFromGS_AskRegiste, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_AskPing] = std::bind(&CCSKernel::OnMsgFromGS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); m_GSMsgHandlerMap[GSToCS::eMsgToCSFromGS_ReportGCMsg] = std::bind(&CCSKernel::OnMsgFromGS_ReportGCMsg, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); m_SSMsgHandlerMap[SSToCS::eMsgToCSFromSS_AskPing] = std::bind(&CCSKernel::OnMsgFromSS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); AddTimer(std::bind(&CCSKernel::ProfileReport, this, std::placeholders::_1, std::placeholders::_2), 5000, true); return eNormal; } ...

2021-01-11

从零学习开源项目系列(四)LogServer源码探究

从零学习开源项目系列(四)LogServer源码探究 这是从零学习开源项目的第四篇,上一篇是《从零学习开源项目系列(三) CSBattleMgr服务源码研究》,这篇文章我们一起来学习LogServer,中文意思可能是“日志服务器”。那么这个日志服务器到底做了哪些工作呢? 我们在Visual Studio中将LogServer设置为启动项,然后按F5将LogServer启动起来,启动成功后显示如下图: 从上图中,我们可以到大致做了三件事: 1. 创建一个侦听端口(端口号1234) 2. 连接mysql数据库 3. 初始化日志处理程序 我们来验证一下这三件事的细节。我们再Visual Studio中将程序中断(【调试】菜单-【全部中断】,快捷键Ctrl + Alt + Break)。然后在线程窗口查看这个程序所有的线程,如下图所示: 所有用红色旗帜标记的线程都是用户线程,我们可以查看这些线程的调用堆栈。我们从最上面的主线程开始: 切换到main函数,我们可以看出这里是一个循环: int main() { auto res = CLogHandler::GetInstance().Init(); if (res) { while(true) { INetSessionMgr::GetInstance()->Update(); Sleep(1); } } return 0; } 这里一个是初始化动作,一个循环中Update动作,它们具体做了些什么,我们先不管,我们先看其他线程做了什么,再回过头来看这里的代码。 我们接着看下一个线程的内容: 从调用堆栈来看,这是一个使用boost::thread启动的线程,这个线程函数代码如下: void Active::Run() { if (m_BeginInThreadCallback){ m_BeginInThreadCallback(); } while (true){ Consume(); } } 我们先看下这个线程函数做了什么,主要是m_BeginInThreadCallback和**Consume()函数,看下Consume()**函数: void Active::Consume(){ boost::mutex::scoped_lock lock(m_IOMutex); while(m_Queue.empty()){ m_ConditionVar.wait(lock); } m_SwapQueue.swap(m_Queue); lock.unlock(); while(!m_SwapQueue.empty()){ Buffer* pBuffer = m_SwapQueue.front(); m_SwapQueue.pop(); m_Callback(pBuffer); --m_PendingWorkNum; if (pBuffer){ m_pBufferPool.ReleaseObejct(pBuffer); } } } 这段代码很好理解,先使用条件变量挂起当前线程,条件变量触发后,如果消费者和生产者共有队列m_Queue中有数据,将公用的队列m_Queue临时倒换到本地的一个局部队列m_SwapQueue中,然后挨个处理队列m_SwapQueue中的数据。 这个线程在哪里创建的呢?通过搜索线程函数,我们找到如下代码: void Active::Start(){ bool ifHvTimer = !m_ThreadTimer.IsEmpty(); if (ifHvTimer){ m_Thread = boost::thread(&Active::RunWithUpdate, this); } else{ m_Thread = boost::thread(&Active::Run, this); } m_ThreadID = get_native_thread_id(m_Thread); char sThreadName[30]; sprintf(sThreadName, "%s-%d", "Actor-Run", GetActorID()); _SetThreadName(m_ThreadID, sThreadName); } 在上面这个函数中添加断点,重启下程序,很快会触发断点,我们看下断点触发时的调用堆栈: ...

2021-01-11

从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始

从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始 写在前面的话 经常有学生或者初学者问我如何去阅读和学习一个开源软件的代码,也有不少朋友在工作岗位时面对前同事留下的项目,由于文档不完善、代码注释少、工程数量大,而无从下手。本文将来通过一个多人联机实时对战游戏——最后一战,来解答以上问题。 其实,我以上问题在我是一个学生时,我也同样因此而困惑,但是后来,我发现,对于文档缺失、注释缺失的项目,需要自己摸索,虽然是挑战,同时也是机遇——一个不错的学习机会。因为至少有代码,正如侯捷大师所说的的,“源码面前,了无秘密”,所以我们应该**“read the fucking code”**。 所以,这个系列的文章,我们分析“最后一战”这个游戏源码时,我们不会按照传统的思路:先介绍总结的程序结构,再介绍各个模块的细节,因为,当我们面对一套陌生的源码时,尤其是在文档缺失的情况下,我们根本无法开始就掌握这个项目的总体结构,我们只能从零开始一个个模块的对代码进行阅读和调试,所以我们这个系列的文章也按这个思路来分析,以真实的案例来教会新手一步步读懂一个开源项目的代码。 我们先来看下这个游戏的内容吧,下面给出游戏画面的部分截图: 这是一款类似于王者荣耀、dota之类的5v5实时RPG竞技游戏。 客户端的逻辑比较简单,主要是一些游戏特效和动画(基于Unity 3D),所以这里我们主要分析游戏的服务器端源码。 先介绍一下推荐的源码的运行和开发环境(我的配置): Windows 7 Visual Studio 2010 服务器端有非常多的模块,这里先截一张主要模块的项目图示: 从下一篇文章开始,我们将介绍如何学习这样的源码。 欢迎阅读下一篇**《从零学习开源项目系列(二) 最后一战概况》**。 源码下载方法: 微信搜索公众号**『高性能服务器开发』(中文名:高性能服务器开发),关注公众号后,在公众号中回复『英雄联盟』**,即可得到下载链接。(喷子和代码贩子请远离!)

2021-01-11

从抓包的角度分析connect()函数的连接过程

从抓包的角度分析connect()函数的连接过程 这篇文章主要是从tcp连接建立的角度来分析客户端程序如何利用connect函数和服务端程序建立tcp连接的,了解connect函数在建立连接的过程中底层协议栈做了哪些事情。 tcp三次握手 在正式介绍connect函数时,我们先来看一下tcp三次握手的过程,下面这个实验是客户端通过telnet远程登录服务端的例子,telnet协议是基于tcp协议,我们可以通过wireshark抓包工具看到客户端和服务端之间三次握手的过程,12.1.1.1是客户端的ip地址,12.1.1.2是服务端的ip地址。 下面是我们通过wireshark抓取到的tcp三次握手的数据包: 我们看到客户端远程登录服务端时,首先发送了一个SYN报文,其中目标端口为23(远程登录telnet协议使用23端口),初始序号seq = 0,并设置自己的窗口rwnd = 4128(rwnd是一个对端通告的接收窗口,用于流量控制)。 然后服务端回复了一个SYN + ACK报文,初始序号seq = 0,ack = 1(在前一个包的seq基础上加1),同时也设置自己的窗口rwnd = 4128。 然后客户端收到服务端的SYN + ACK报文时,回复了一个ACK报文,表示确认建立tcp连接,序号为seq = 1, ack = 1*(在前一个包的seq基础上加1)*, 设置窗口rwnd = 4128,此时客户端和服务端之间已经建立tcp连接。 connect函数 前面我们在介绍tcp三次握手的时候说过,客户端在跟服务端建立tcp连接时,通常是由客户端主动向目标服务端发起tcp连接建立请求,服务端被动接受tcp连接请求;同时服务端也会发起tcp连接建立请求,表示服务端希望和客户端建立连接,然后客户端会接受连接并发送一个确认,这样双方就已经建立好连接,可以开始通信。 这里说明一下:可能有的小伙伴会感到疑惑,为啥服务端也要跟客户端建立连接呢?其实这跟tcp采用全双工通信的方式有关。对于全双工通信,简单来说就是两端可以同时收发数据,如下图所示: 我们再回到正题,那么在网络编程中,肯定也有对应的函数做到跟上面一样的事情,没错,就是connect(连接)。顾名思义,connect函数就是用于客户端程序和服务端程序建立tcp连接的。 一般来说,客户端使用connect函数跟服务端建立连接,肯定要指定一个ip地址和端口号(相当于客户端的身份标识),要不然服务端都不知道你是谁?凭什么跟你建立连接。同时还得指明服务端的ip地址和端口号,也就是说,你要跟谁建立连接。 connect函数原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明: sockfd:客户端的套接字文件描述符 addr:要连接的套接字地址,这是一个传入参数,指定了要连接的套接字地址信息(例如IP地址和端口号) addrlen:是一个传入参数,参数addr的大小,即sizeof(addr) 返回值说明:连接建立成功返回0,失败返回-1并设置errno connect函数在建立tcp连接的过程中用到了一个非常重要的队列,那就是未决连接队列,这个队列用来管理tcp的连接,包括已完成三次握手的tcp连接和未完成三次握手的tcp连接,下面我们就来详细介绍一下未决连接队列。 未决连接队列 未决连接队列是指服务器接收到客户端的连接请求,但是尚未被处理(也就是未被accept,后面会说)的连接,可以理解为未决连接队列是一个容器,这个容器存储着这些尚未被处理的链接。 当一个客户端进程使用 connect 函数发起请求后,服务器进程就会收到连接请求,然后检查未决连接队列是否有空位,如果未决队列满了,就会拒绝连接,那么客户端调用的connect 函数返回失败。 如果未决连接队列有空位,就将该连接加入未决连接队列。当 connect 函数成功返回后,表明tcp的“三次握手”连接已完成,此时accept函数获取到一个客户端连接并返回。 在上图中,在未决连接队列中又分为2个队列: 未完成队列(未决队列):即客户端已经发出SYN报文并到达服务器,但是在tcp三次握手连接完成之前,这些套接字处于SYN_RCVD状态,服务器会将这些套接字加入到未完成队列。 已完成队列:即刚刚完成tcp三次握手的tcp连接,这些套接字处于ESTABLISHED状态,服务器会将这些套接字加入到已完成队列。 我们来看一下连接建立的具体过程,如图所示: 服务端首先调用listen函数监听客户端的连接请求,然后调用accept函数阻塞等待取出未决连接队列中的客户端连接,如果未决连接队列一直为空,这意味着没有客户端和服务器建立连接,那么accept就会一直阻塞。 当客户端一调用connect函数发起连接时,如果完成tcp三次握手,那么accept函数会取出一个客户端连接(注意:是已经建立好的连接)然后立即返回。 上面就是客户端和服务端在网络中的状态变迁的具体过程,前面我们在学习tcp三次握手的过程中还知道,服务端和客户端在建立连接的时候会设置自己的一个接收缓冲区窗口rwnd的大小。 服务端在发送SYN + ACK数据报文时会设置并告知对方自己的接收缓冲区窗口大小,客户端在发送ACK数据报文时也会设置并告知对方自己的接收缓冲区窗口大小。 注意,accept函数调用成功,返回的是一个已经完成tcp三次握手的客户端连接。如果在三次握手的过程中(最后一步),服务端没有接收到客户端的ACK,则说明三次握手还没有建立完成,accept函数依然会阻塞。 关于tcp三次握手连接建立的几种状态:SYN_SENT,SYN_RCVD,ESTABLISHED。 SYN_SENT:当客户端调用connect函数向服务端发送SYN包时,客户端就会进入 SYN_SENT状态,并且还会等待服务器发送第二个SYN + ACK包,因此SYN_SENT状态就是表示客户端已经发送SYN包。 SYN_RCVD:当服务端接收到客户端发送的SYN包并确认时,服务端就会进入 SYN_RCVD状态,这是tcp三次握手建立的一个很短暂的中间状态,一般很难看到, SYN_RCVD状态表示服务端已经确认收到客户端发送的SYN包。 ESTABLISHED:该状态表示tcp三次握手连接建立完成。 对于这两个队列需要注意几点注意: 1. 未完成队列和已完成队列的总和不超过listen函数的backlog参数的大小。listen函数的签名如下: int listen(int sockfd, int backlog); 2. 一旦该连接的tcp三次握手完成,就会从未完成队列加入到已完成队列中 3. 如果未决连接队列已满,当又接收到一个客户端SYN时,服务端的tcp将会忽略该SYN,也就是不会理客户端的SYN,但是服务端并不会发送RST报文,原因是:客户端tcp可以重传SYN,并期望在超时前未决连接队列找到空位与服务端建立连接,这当然是我们所希望看到的。如果服务端直接发送一个RST的话,那么客户端的connect函数将会立即返回一个错误,而不会让tcp有机会重传SYN,显然我们也并不希望这样做。 ...

2021-01-11

多线程

多线程 后台C++开发你一定要知道的条件变量 整型变量赋值是原子操作吗?

2021-01-11