最近在学习golang,完成了一个聊天app,效果如下:
整个项目后端基于golang,主要通过websocket来实现。根据websocket的协议,在socket连接之后需要进行ping-pong发包,ping-pong应该由服务器自己实现,无需客户端的参与,用来保证websocket连接的正常,下面我们用golang来实现一下。
首先构建如下测试项目结构:
1 | . |
在main.go中构建一个http服务:
1 |
|
在session.go中需要实现两个goroutine,一个readLoop用来监听客户端发给server的信息,一个writeLoop用来监听服务端发给客户端的信息。
1 |
|
在writeLoop中需要定义一个NewTicker,它的作用是每间隔一段固定的时间,将当前的时间发送到channel ticker.C中,这样我们可以通过监听该通道,用来在固定时间内向客户端发送一个ping消息。当websocket收到ping消息之后在readLoop中实现setPongHandler函数,用来向server返回一个事件,用来表示当前客户端连接并未断开。这个事件就是SetReadDeadline这个事件,它用来表示该websocket在几秒之后会失效(当超过该时间后会使websocket断开)。所以pong处理时需要更新websocket的ReadDeadline的时间,保证socket的长期连接。
在webcok_hdl.go中将http请求升级成websocket:
1 |
|
注意:在golang中函数与方法是有区别的,一个函数可以拥有自己的方法,一个方法会有自己的接受者,这与传统的面向对象的语言不同。所以我们可能会看见一些奇怪的使用方法,比如定义一个函数类型:
1 | type HandlerFunc func(ResponseWriter, *Request) |
定义一个Hnadler接口:
1 | type Handler interface { |
给HandlerFunc函数类型实现Handler接口中的方法:
1 |
|
在golang的net/http内置包中的HandlerFunc函数(用来将路由映射到视图函数)就运用了上述的技巧,使你能够将任意命名的函数只要参数是(ResponseWriter,*Resquest),就均能作为视图函数,来处理路由。如下:
1 | server.HandleFunc("/ws", WebsocketHandler) |
在源码中跟踪上述函数:
1 |
|
在HandleFunc中调用了Handle函数:
1 | func (mux *ServeMux) Handle(pattern string, handler Handler) |
Handle函数的第二个参数应该是一个Handle接口:
1 |
|
注意在Handle函数中,第二个参数使用了一个HandlerFunc函数(多一个r),这个函数的定义如下:
1 |
|
可见,这里的HandlerFunc(handler))可以理解为强制类型转化,将handler函数强制转化为Handler接口,这样就能够实现上述功能。
defer在go中是一个全新的关键字,他的作用在于能够在go函数返回之后,延迟加载defer函数中的内容。这里对defer的作用进行如下测试:
1 | func test2() { |

2.函数中有多个defer函数
当函数中有多个defer时,从上到下,根据“先进后出”的原则 得到结果。
1 | func test2() { |

3.当defer与return均存在时,return先返回结果,再执行defer中的内容。实验如下:
1 | var name string = "go" |

1.单个变量声明 var name string 这样声明一个变量,注意如果没有赋初始值,那么字符串初始值为空字符串“”,int为0,float为0.0,bool为false,指针类型为nil。
在go中,单引号和双引号有区别。在初始化变量时,如果右值带有小数点,那么建议带上类型float32。
2.多个变量一起声明,声明多个变量
1 | var ( |
3.匿名变量的优点

在go中最好不要直接使用int给变量作为type,原因在于在32位机器上,int表示4个字节相当于int32,在64位机器上,int 表示8个字节,相当于int64。
二进制表示一个数:0b110
八进制表示一个数: 0o14
十六进制表示一个数:0xC
fmt格式化代码:
1 | %b 格式化为二进制, |
浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。
其中,整数部分和小数部分均由10进制表示法表示。不过还有另一种表示方法。那就是在其中加入指数部分。指数部分由“E”或“e”以及一个带正负号的10进制数组成。比如,3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37。
有一点需要注意,在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。比如,03.7表示的一定是浮点数3.7。
Go语言中提供了两种精度的浮点数 float32 和 float64。
float32,也即我们常说的单精度,存储占用4个字节,也即4*8=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数。

float64,也即我们熟悉的双精度,存储占用8个字节,也即8*8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数。

浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:
注意浮点数表示精度的问题:
float32的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
float64的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度
byte用来表示一个ASCII码字符,rune占有四个字节,表示一个unicode字符。
单引号用来表示字符,如果你使用双引号,就意味着你要定义一个字符串,赋值时与前面声明的前面会不一致,这样在编译的时候就会出错。
byte 和 uint8 没有区别,rune 和 uint32 没有区别,那为什么还要多出一个 byte 和 rune 类型呢?
理由很简单,因为uint8 和 uint32 ,直观上让人以为这是一个数值,但是实际上,它也可以表示一个字符,所以为了消除这种直观错觉,就诞生了 byte 和 rune 这两个别名类型。
字符串一般用双引号表示””,但是字符串也可以用``反引号,在反引号中,会忽略转义字符。
数组是一个由固定长度的特定元素类型组成的序列。
注意:
name := [...]int{}类型的方式来让go自动识别大小。type关键字:1 | type arr [3]int |
数组 与 切片 有相同点,它们都是可以容纳若干类型相同的元素的容器
也有不同点,数组的容器大小固定,而切片本身是引用类型,它更像是 Python 中的 list ,我们可以对它 append 进行元素的添加。
取切片值:
1 | m := make([]int, 2, 10) |
字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在,若存在ok为true,若不存在,则ok为false
1 | //******************************************** |
关于布尔值,无非就两个值:true 和 false。只是这两个值,在不同的语言里可能不同。
而在 Go 中,真值用 true 表示,不但不与 1 相等,并且更加严格,不同类型无法进行比较,而假值用 false 表示,同样与 0 无法比较。
指针的概念学了c语言将不难理解,变量分为普通变量和指针变量;
在golang中if-else if -else有以下特点:
}else{ }else if{必须和左右括号在同一行注意:
for 循环的基本模型
1 | for [condition | ( init; condition; increment ) | Range] |
注意:
for{}(在for后面不加任何表达式来控制无线循环)for range遍历可以可迭代对象。for range的for后面不能添加任何表达式不建议使用,使用方法如下:
1 | goto flag: |
tornado框架自身没有集成session鉴权,但是对cookie支持良好。由于现在前端vue部署在一个端口上,后端tornado分布式应用部署在多个端口上,这就会导致浏览器的跨域问题产生(url不同,即协议,域名,端口三者有不相同的地方)。
在前后端分离的项目中,由于页面的路由跳转由vue控制,后端应用只提供数据。这样就会使鉴权任务变得较为复杂。一个比较良好的解决思路是,在前端需要鉴权才能访问的路由中添加一个后端异步请求,该请求用来判断用户是否登录,这样来判断用户当前是否有权限访问该页面。同时,后端需要 登录才能访问的接口,用修饰器来判断用户当前是否登录。下面提供一种tornado实现session的解决方案。
在新建一个HTTP请求时,需要初始化一个session类,该类应该实现以下功能:
能够从请求头的cookie中得到session_id;
能够生成一个全新的session_id,并将其返回给client;
若server得到的session_id失效,则清空client的cookie;
若server得到的sess_id有效,则从redis中取出对应的用户信息
在一个HTTP请求结束时,需要判断,当前这个链接是否为”登录成功“的API,如果是,则需要将对应的用户信息存储到redis中,并生成一个新的session_id返回给用户。
代码如下:
BaseHandler
1 |
|
login API
1 |
|
微服务的概念网上都有,各种博客都有解释。但是自己没有上手写代码,没有做工程项目,实际上是很难体会的。 微服务与SOA(面向服务的架构)的区别主要在于微服务中每个服务是一个独立的进程,它能够被独立的部署。若该服务宕机,并不影响整个系统中其他服务的正常运行。
在开始尝试做一个新的系统时,若常用微服务的方式,则需要使用限界上下文划分微服务边界。限界上下文指的是一个由显式边界限定的特定职责。如果你想要从一个限界上下文中获取信息,或者向其发起请求,需要使用模型和它的显式边界进行通信。实际上寻找限界上下文边界就是将整个业务逻辑进行一个粗粒度的划分。对于一个新系统而言,过早划分很容易错误判断服务之间的边界,按照书中内容所说,很多时候,将一个已有的代码库划分成微服务,要比从头开始构建微服务简单得多。
当你在思考组织内的限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下文能够提供的功能来考虑。服务与服务之间如果能够做到低耦合,高内聚,那么这就是一个好的微服务系统。实际上,要做到低耦合,高内聚还是很困难的,在工程项目中很可能遇到一份代码需要多次拷贝使用,当遇到这种情况时,就需要注意DRY(don’t repeat yourself)。应该考虑使用更好的设计模式来避免这种情况,因为很容易出现,一个地方被修改了,但是另一个地方忘记修改的情况。
服务集成是微服务相关技术中最重要的一个问题,按照作者的话来说:做得好的话,你的微服务可以保持自治性,你也可以独立地修改和发布它们;但做得不好的话会带来灾难。在选择集成方式时,需要注意以下几个原则:1.避免破坏性修改 2. 保证API的技术无关系3.使你的服务易于消费方使用4.隐藏内部实现细节。服务之间的通信可以分为同步和异步两种:如果使用同步通信,发起一个远程服务调用后,调用方会阻塞自己并等待整个操作的完成。如果使用异步通信,调用方不需要等待操作完成就可以返回,甚至可能不需要关心这个操作完成与否。这两种不同的通信模式有着各自的协作风格,即请求/响应或者基于事件。对于请求/响应来说,客户端发起一个请求,然后等待响应。这种模式能够与同步通信模式很好地匹配,但异步通信也可以使用这种模式。我可以发起一个请求,然后注册一个回调,当服务端操作结束之后,会调用该回调。对于使用基于事件的协作方式来说,情况会颠倒过来。客户端不是发起请求,而是发布一个事件,然后期待其他的协作者接收到该消息,并且知道该怎么做。
服务与服务之间常见有两种常见的架构风格:编排与协同。

对于上图用户注册这样一个业务逻辑,如果用编排的风格来设计服务,我们可以得到如下的架构图:

很显然,若编排的架构使用的是同步通信的机制,即请求/响应的协作方式,那么我们能够得到每一个服务返回的结果。当然,编排的架构也能使用基于回调的异步通信。编排架构的缺点在于,客户服务承担了太多的职责,而剩下的三个服务按作者的话来说:会成为贫血的CRUD服务。
如果采用协同的架构风格,我们可以得到下面这个架构图:

可以仅仅从客户服务中使用异步的方式触发一个事件,该事件名可以叫作“客户创建”。电子邮件服务、邮政服务及积分账户可以简单地订阅这些事件并且做相应处理,如上图所示,这种方法能够显著地消除耦合。使用协同的方式组织服务,很显然必定是异步的。使用这种发布-订阅的模式,是当前非常流行的一种通信模式,典型的就是消息队列,比如RabbitMQ。这种模式能够很大程度的在服务之间进行解耦,但是它的一个问题在于无法检测服务是否成功执行,比如电子邮件服务出现问题,无法发送邮件,此时整个系统是否能够检测到该问题,并进行处理。一个常见的解决方案是构建一个与业务逻辑流程图相匹配的监控系统。实际的监控活动是针对每个服务的,但最终需要把监控的结果映射到业务流程中。针对请求响应的协同方式,常用有两种技术:RPC和REST(表征性状态转移)风格的HTTP。针对基于事件的协同方式,一般采用消息队列来实现。
微服务的部署相对于单体应用来说,也是一个较为复杂的问题。CI/CD(持续集成/持续部署)是当前非常重要的一种思想。按照书中所说,判断你的项目是否CI,可以通过回答一下几个问题来判断:1.你是否每天签入代码到主线?2.你是否有一组测试来验证修改?3.当构建失败后,团队是否把修复CI当作第一优先级的事情来做?我个人感觉CI/CD应该是用来解决开发和运维之间的矛盾,应该需要一个DevOps系统来实现这种思想。
]]>在网络通信中,数据的传输大部分基于socket(socket位于TCP/UDP与HTTP协议之间,当然socket也可以利用SOCK_RAW套接字提供一个数据报接口,用于直接访问下面的网络层,当使用SOCKT_RAW套接字时,应用程序应该构造自己的协议头部)。在unix系统中,将socket的操作抽象为文件的读,写,异常,即一个socket对象可以对应为一个文件描述符(fd)。所以网络I/O与磁盘读写O/I可以属于同一层次上要考虑的问题。
在考虑具体问题之前,首先要明确一个概念,阻塞与非阻塞是对于进程而言:当一个进程想要read一个文件描述符(fd)时,若fd的读缓冲区没有数据,此时进程将会阻塞,直到缓冲区中有数据可以使用,此时进程才会继续向下运行,这种情况便是阻塞I/O。对非阻塞I/O而言,当fd的读缓冲区中没有数据时,进程并不会阻塞在此处,而是立即返回错误,表示该操作如继续执行将会阻塞。下图是一个非阻塞I/O的例子(a.out)

执行以下代码:
./a.out < /etc/services 2>stderr.out
可以得到如下结果:

其中35对应的是EAGAIN信号,显然一次输出到终端的数据是有限的,当数据不能write到终端时,进程并没有阻塞,而是返回信号EAGAIN,表明当前写缓冲区已满, 以上代码便是一种轮询操作。
考虑如下一种场景:当该进程必须要从两个或多个fd中同时读取数据时,在这种情况下,我们不能将进程阻塞在任何一个fd的read方法上,原因是,另一个fd可能已经准备好read了,但进程阻塞,无法对其进行任何操作。为了能够使进程同时处理多个fd,我们的第一个想是fork子进程,或者用多线程同时阻塞处理。但这在实现上较为繁琐,且占用资源。另外,也可以用轮询的方法来遍历fd的状态。第三种方法是用异步I/O:进程告诉内核,当fd准备好可以进行I/O时,用一个信号通知进程,这种方法存在的问题在于:每个进程只能有一个信号,如果信号对多个fd都起作用,那么进程在收到该信号时,无法判断是哪个fd准备就绪。以上三种方法或多或少都有问题,我们主要考虑I/O多路转接技术。
I/O多路转接技术想要实现的作用是:将想监听的fd加入一个列表,用某个函数来监听这个列表,当列表中有fd准备就绪时,返回准备就绪的fd给进程使用。select函数和poll函数便是实现该功能。select函数如下:
传给select的参数告诉内核:
我们所关心的描述符
对于每个描述符,我们所关心的条件
愿意等待多长时间
select将会返回:
已经准备好的描述符的总数量
对于读,写,异常,哪些描述符已经准备好了。
该函数返回的fd一定是准备就绪的,调用对应的read,write函数一定不会发生阻塞。select函数的具体实现过程:

1 |
|
内核创建一个epoll对象,epoll_ctl函数向epoll对象中添加需要监听的fd,当fd准备就绪时,中断程序会操作epoll对象,而不是操作进程。当进程执行到epoll_wait时,如果就绪列表存在已经准备好的fd,进程将会被唤醒。

epoll的作用是提升事件循环查询“io事件”的效率,他允许用户进程同时监听多个文件描述符的io事件即io多路复用。同时epoll在实现上是十分高效的,相对于poll/select,epoll使得进程可以监控更多的文件描述符。epoll采用注册机制,在内核中保存用户关注的文件描述符(红黑树保存),不像select一样每次都需要传入所有的文件描述符,效率更高(减少了从用户空间向内核空间的拷贝)。epoll不像select一样会在内核中轮询所有的文件描述符(低效的),epoll会在不同文件描述符对应的设备的等待队列中添加一个回调函数,在回掉函数中将对应的文件描述符添加至epoll的rdlist队列(高效的),当进程调用epoll_wait时,内核检查该队列是否为空,如果是空则阻塞进程,如果不为空则将该队列拷贝至用户空间,并返回该函数。epoll的两种触发模式:
1.水平触发:只要fd仍然可读或写,每次调用epoll_wait都会返回该文件描述符;
2.边缘触发:只fd仍然可读或写,每次调用epoll_wait只会返回一次该文件描述符;
注意异步I/O与非阻塞I/O的区别,异步I/O,需要用回调函数来处理,fd准备好之后的情况,比较复杂。而非阻塞则是在fd的read,write操作时,一定可以立即得到结果,不会阻塞进程。
]]>1.监督学习
回归: 预测连续值
分类:预测离散值
2.无监督学习
聚类算法
鸡尾酒算法
梯度下降

正确更新方式是同时更新x1和x2
计算偏导数
logistics 函数

logistics代价函数
leetcode94
问题:
栈的方式实现二叉树
1 | class Solution: |
关键点:
1.根节点先入栈,然后以左节点为根节点,直到左节点为空,将根节点出栈。 以右节点为根节点
leetcode572
问题:判断t是否为s的子树
1 | python解决方案 |
注意点:
二叉树右视图(leetcode 199)
1 | class TreeNode: |
思路:
1 | 先序遍历,注意rightSideView(r->left, i+1)与rightSideView(r-right, i+1)中的i相同,由此可以知道,字典中存储的数据一定是每层的最后一个数据 |
1 | +表示public |
1 | 1.依赖关系 |
python导包时的问题
1.可以使用sys.path,查看python导包的搜素路径;使用sys.modules来查看已经导入内存的模块.
一个细节点: import 模块 import只能导入模块(.py, .pyc,pyd),不能导入模块中的类和函数。如果需要导入模块中的函数或者对象,使用from 模块 import 对象
即为
1 | 说一个容易忽略的问题,import只能导入模块,不能导入模块中的对象(类、函数、变量等)。如一个模块A(A.py)中有个函数getName,另一个模块不能通过import A.getName将getName导入到本模块,只能用import A。如果想只导入特定的类、函数、变量则用from A import getName即可。 |
2.嵌套导包
(1)在A.py中导入import B, 在B.py中导入了import C,则在A中只能使用B而不能使用C,即时C已经存在于内存之中。也就是说必须显示导包
(2)第二种情况
1 | [A.py] |
在A中执行C()时,将会报错, 而如果改成import B则可以导入,解释如下:
1 | 另外一种嵌套指,在模块A中import B,而在模块B中import A。这时会怎么样呢?这个在Python列表中由RobertChen给出了详细解释,抄录如下: |
导入包:
1 | Package(包) Import |
问题: 已知序列str1和序列str2,求出他们的最长公共子序列。
dp[i][j]的定义: str[0-i]和str[0-j]所拥有的最长公共子序列。
法一:
1 | def length_common_strength(str1, str2) -> int: |
法二:
1 | def length_common_strength_dp(str1, str2) -> int: |
问题: 求无序序列的最长子序列
方法: 动态规划法
关键点:
1 | dp[i] : 表示数组中的第i位置的最长子序列的长度,初始默认所有位置的dp[i] = 1 |
思路: 假设已知Subsequence[i-1]位置的最长子序列为dp[i-1],则dp[i],显然如果subsequence[i]>subsequence[i-1],则dp[i] = dp[i-1] + 1
代码:
1 | #include <cstdio> |
斐波拉契数,最基本的定义用递归,现利用”自底向上”的思路求解:
1 | #include <cstdio> |
不相交子区间问题
1 | /* |
全排列

递归算法求全排
1 | /* |
n皇后问题
1 |
|
hash
基本问题
1 | #include <cstdio> |
字符串hash
大写字符hash核心代码:
1 | for(int i=0; i<len(s): ++i){ |
大小写字母均hash
1 | for(int i=0; i<len(s): ++i){ |
bubbleSort
关键点:一轮能够确定一个数,内层循环有:a[j]>a[j+1],保证a[j+1]不溢出,第一轮循环需要从i=1开始,内层循环从j=0开始,j<n-i
1 | void BubbleSort(int a[], int n) {//冒泡排序,a为数据数组, n为数组元素个数 |
selectSort
关键点:与bubble刚好相反,它是从前往后排,所以关键在内层循环的int j=i开始点,而冒泡则是内层循环的j<n-i结束的点
1 | void selectSort(int a[], int n) {//选择排序 |
求幂集问题(穷举法)
1 | 给定一个数n,求出从1-n的所有子集 |
法一:直接穷举
1 | #include <stdio.h> |
法二:增量穷举法
1 | #include <stdio.h> |
示例图:
命题: 能判断真假的陈述句称为命题
1 | 1. 感叹句,祈使句,疑问句都不是命题 |
简单命题: 由一个简单陈述句组成(原子命题,不能再分)
复合命题: 简单命题 + 联结词
五种常用的联结词:否定,合取,析取,蕴涵,等价
否定: 非,并非
合取
合取词: “且”
P 且 Q
当且仅当P和Q同时为真
1 | P并且Q |
析取
析取词: 或
P或Q: 为假,当且仅当P和Q同时为假
分类: 相容或,排斥或
蕴涵词
如果.. 那么 ..