大型网站架构演化

大型网站架构演化

网站架构设计的目标

高可用、高性能、易扩展、可伸缩、安全。

网站的价值在于它能为用户提供什么价值,在于网站能做什么,不在于网站怎么做的。
不是所有的问题都可以通过技术来解决。
有的时候调整业务也是很好的一种方式。
技术是用来解决业务问题的,业务的问题也可以尝试通过业务的手段去解决。

网站架构演化发展历程

初始阶段

在这里插入图片描述

应用服务和数据服务分离

一台服务器逐渐不能满足需求,越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足。
将存储和应用服务器分开。
在这里插入图片描述

缓存

80% 的业务访问集中在 20% 的数据上。
缓存又可以有两种方案:

  1. 缓存在应用服务器的本地缓存;
    1. 访问速度较快,但是收到内存限制。
  2. 专门的分布式缓存服务器;
    1. 理论上不受内存容量限制。

在这里插入图片描述

应用服务器集群

使用集群是网站解决高并发、海量数据问题的常用手段。

在这里插入图片描述

数据库读写分离

网站使用缓存后,绝大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作(缓存访问不命中、缓存过期)和全部的写操作需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。
大部分主流数据库提供主从热备功能,配置主从关系,将数据同步,利用这一功能进行读写分离,改善数据库负载压力。
在这里插入图片描述

反向代理和 CDN 加速

CDN 和 反向代理的基本原理都是缓存。
CDN 部署在网络提供商的机房,用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;
反向代理部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器是反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。
在这里插入图片描述

分布式文件系统和分布式数据库

文件服务器正常,文件太多放不下了。
分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常大的时候才使用。
不到不得已时,网站常用的数据库拆分手段是业务分库,不同业务使用不同数据库部署在不同的机器上。

在这里插入图片描述

NoSQL 和搜索引擎

网站业务愈加复杂,对数据存储和检索的需求也越来越复杂,网站需要使用一些菲关系数据库技术如 NoSQL 和非数据库查询技术如搜索引擎。
在这里插入图片描述

业务拆分

纵向拆分。
在这里插入图片描述

服务拆分

横向拆分。
提供一些统一的常用的相同的业务操作。
业务部门、基础服务器部门、数据部门等等。

在这里插入图片描述


EOF

阅读全文
缓存雪崩、缓存击穿、缓存穿透

缓存雪崩、缓存击穿、缓存穿透的概念和解决方案。

布隆过滤器的简单原理和应用。

缓存雪崩

就是缓存层里的数据同一个时间点失效,那么这些数据就会集中打向 MySQL。

缓存击穿

缓存里有一条数据,一条数据失效后也是穿过了 redis 打到了 MySQL。

缓存击穿、缓存雪崩属于缓存穿透的一种特殊表现形式。

缓存穿透

描述

当找 redis 找不到数据时候,就会出现缓存穿透。

低频率的缓存穿透不可怕,正常现象。

当高频率的穿透就影响大了。

黑客攻击的场景,模拟很多客户端发送 id = -1 的请求。

id = -1 在 redis 中找不到,穿过 redis 找到数据库,数据处理不过来就崩了(拒绝服务攻击DDOS)。

解决方案

  • 通过 MySQL 中找不到的值缓存在 redis 中。(黑客太菜了)

    当然第一次查询 id = -1 的时候可能没找到穿透了,但是这次我把 id = -1 放在了 redis 中。

    id = -2 再查询我再放。。。。也是大量的访问数据库。(UUID 黑盒 每次生成一个不一样的 ID)

    这个方案适得其反,redis 一定的数据淘汰策略(LRU、LFU…)如果每次都是 UUID,那么真正需要缓存的数据就被淘汰了,redis 里是一些垃圾数据。

  • 过滤器(redis),这个过滤器把 MySQL 里面的所有 id 号,放在 redis 和 MySQL 的中间(不行),不一定是通过 id 查询或者 id 过多,这样就会导致过滤器的效率太低,内存紧张,导致整个链路都很慢。

    解决过滤器中数据过多的场景。布隆过滤器,通过一定的错误率降低内存的占用。

    比如 id = 100,传给 hash 函数,来一个 bin(二进制, size=10) 数组,保证哈希结果在 0-9 之间。。。。。。

    拿到 id = 100,计算出的 hash 相同,就会告诉客户端你要的数据是有的。

    错误发生在计算哈希上,如果布隆过滤器告诉你数据存在,那么数据不一定存在。如果布隆过滤器告诉你数据不存在,那么这个数据一定不存在。

    宁可错杀 3k 不放过一个。发生哈希碰撞就有可能是一次错误,数组的长度影响错误率

    哈希函数的个数也有关系。布隆过滤器的简单实现是 3 个 hash 函数计算的三个位置标示一个数据是否存在。哈希函数个数多了反而错误率升高。数组长度很长会增加内存占用。

    【人生中最重要的一个字“度”】,通过算法计算合适的 hash 函数个数和数组长度。通常的布隆过滤器会要求提供允许的错误率 fpp 和存储的数据量 n。然后通过公式计算出数组大小 m 和 hash 函数的个数 k。

    m = - ( n*ln(fpp) ) / ( ln(2) ^2 )

    k = m/n * ln(2)

遇到删除数据的情况?如果 id = 100 和 id = 10 的 hash 结果相同,我删除了 id = 10 的数据,那么布隆过滤器的这个 hash 结果的位置不能设为 0,因为还有数据对应。

如果场景中有频繁的数据删除情况,建议搞一个二维的数组,第二维的数组用来计数。(长度 100亿占用 1GB 左右)

面试题:文件 A 存了 100 亿个URL,B 文件也是 100 亿个 URL。只给一个 4GB 内存文件,快速找出 AB 的交集。

模糊算法

通过布隆过滤器来计算 A 中的,B 中的 URL,然后找到对应的交集。只需要一次的磁盘 IO 即可。

精准算法

A、B 两个大文件拆分 1000 个放一个文件,分块计算 hash 值然后对 1000 取余。然后对A、B的小文件一一比较。拆分成小文件是为了可以加载入内存中。

相同 URL 的 hash 值相同,取摸后也相同。所以会出现在 1-1 对应的小文件中。


EOF

阅读全文
UDP协议、UDP编程、有连接的UDP

基础知识

UDP 输出 TCP/IP 协议分层中的传输层。
UDP 是一个不可靠的通信协议,没有重传和确认,没有有序控制,也没有拥塞控制。
UDP 不保证报文的有效传递,不保证报文的有序,也就是说使用 UDP 的时候,我们需要做好丢包、重传、报文组装等工作。
UDP 比较简单,适合的场景还是比较多的,我们常见的 DNS 服务,SNMP 服务都是基于 UDP 协议的,这些场景对时延、丢包都不是特别敏感。另外多人通信的场景,如聊天室、多人游戏等,也都会使用到 UDP 协议。
OSI和TCP/IP

报文格式

UDP 报头长 8 字节,分别是源端口、目的端口、UDP 报文长度、校验和
因为 UDP 报文长度只用 2 个字节记录,所以包含报头长度的报文最大长度为 65535 字节。

用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) - UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。
由于IP有最大MTU,因此,
UDP 包的大小应该是 1500 - IP头(20) - UDP头(8) = 1472(Bytes)

UDP报文

UDP 编程

服务器:
1、创建 socket。
2、绑定要监听的 ip 和 port。
3、循环:
3.1、调用 recvfrom 读取接收到的报文,如果没有报文则阻塞在这里。
3.2、收到报文处理完后调用 sendto 将相应发给客户端。

客户端:
1、创建 socket。
2、循环:
2.1、调用 sendto 发送请求。
2.2、调用 recvfrom 接收相应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/socket.h>
// 返回值:收到数据的字节数
// 参数:
// sockfd:socket描述符
// buff:本地缓存
// nbytes:缓存最大接收字节数
// flags:I/O 相关的参数,一般使用 0 即可
// from:发送端的 ip 和 port 等信息
// addrlen:from 的大小
ssize_t
recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
          struct sockaddr *from, socklen_t *addrlen);
    
// 返回值:发送了多少字节
// 参数:和上面的 recvfrom 类似      
ssize_t
sendto(int sockfd, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);

UDP编程时序
代码参考我之前的文章:UDP 回显程序https://blog.csdn.net/Hanoi_ahoj/article/details/105358383

UDP 报文的“无连接”的特点,可以在 UDP 服务器重启之后,继续进行报文的发送,这就是 UDP 报文“无上下文”的最好说明。

有连接的 UDP

通过上文,在 UDP 中是不需要类似于 TCP 编程中的 connect 建立连接的。
其实 UDP 也可以是 “有连接” 的。

下面通过一个程序来测试一下:
客户端: 需要注意的是在创建完 socket 后进行了 connect,绑定了服务器的 ip 和 port。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// UDP connect 测试客户端

#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
perror("socket");
return -1;
}

struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9090);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

int ret = connect(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret < 0) {
perror("connect");
return -1;
}

while (1) {
char buf[1024] = {0};
printf("input>");
scanf("%s", buf);
ssize_t n = sendto(socket_fd, buf, strlen(buf), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (n < 0) {
perror("sendto");
continue;
}

printf("%zd bytes sent to [%s:%d]\n", n, inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));

bzero(buf, sizeof(buf));
n = recvfrom(socket_fd, buf, sizeof(buf), 0, NULL, NULL);
if (n < 0) {
perror("recvfrom");
return -1;
}
printf("resp: %s\n", buf);
}

close(socket_fd);
return 0;
}

服务器: 就是正常的服务器,收到请求原封不动返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main() {
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
perror("socket");
return -1;
}

struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
server_addr.sin_port = htons(9090);
int ret = bind(socket_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret < 0) {
perror("bind");
return -1;
}

// 一般服务器不进行 connect 操作
while (1) {
char buf[1024] = {0};
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(client_addr));
socklen_t client_addr_len = sizeof(client_addr);
ssize_t n = recvfrom(socket_fd, buf, sizeof(buf) - 1, 0,
(struct sockaddr*)&client_addr, &client_addr_len);
if (n < 0) {
perror("recvfrom");
continue;
}
buf[n] = '\0';

printf("req->[%s:%d] %s\n", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), buf);

n = sendto(socket_fd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, client_addr_len);
if (n < 0) {
perror("sendto");
continue;
}
}

close(socket_fd);
return 0;
}
1
2
gcc client.c -o client
gcc server.c -o server

测试:
1、不运行 server,只运行 client。
可以看到,在调用 sendto 的时候发送了,但是当走到 recvfrom 的时候出错 Connection refused。

client
2、运行 server 和 client。
正常的接收请求、处理请求过程。
server && client

UDP connect 的作用

不用 connect 的话,不开启 server,运行 client,程序会阻塞在 recvfrom 上。直到服务器重启或者超时。
通常 UDP 的服务器是不需要进行 connect 的,因为 connect 以后这个服务器就只能对这个客户端进行服务器了。

==connect 的作用就是让程序尽早收到错误信息返回:==

通过对 UDP 套接字进行 connect 操作,将 UDP 套接字建立了“上下文”,该套接字和服务器端的地址和端口产生了联系,正是这种绑定关系给了操作系统内核必要的信息,能够将操作系统内核收到的信息和对应的套接字进行关联。

调用 sendto 或者 send 操作函数时,应用程序报文被发送,我们的应用程序返回,操作系统内核接管了该报文,之后操作系统开始尝试往对应的地址和端口发送,因为对应的地址和端口不可达,一个 ICMP 报文会返回给操作系统内核,该 ICMP 报文含有目的地址和端口等信息。

进行了 connect 操作,帮助操作系统内核从容建立了(UDP 套接字——目的地址 + 端口)之间的映射关系,当收到一个 ICMP 不可达报文时,操作系统内核可以从映射表中找出是哪个 UDP 套接字拥有该目的地址和端口,别忘了套接字在操作系统内部是全局唯一的,当我们在该套接字上再次调用 recvfrom 或 recv 方法时,就可以收到操作系统内核返回的“Connection Refused”的信息。

在对 UDP 进行 connect 之后,关于收发函数的使用,很多书籍是这样推荐的:
使用 send 或 write 函数来发送,如果使用 sendto 需要把相关的 to 地址信息置零;
使用 recv 或 read 函数来接收,如果使用 recvfrom 需要把对应的 from 地址信息置零。
其实不同的 UNIX 实现对此表现出来的行为不尽相同。

==效率因素:==

因为如果不使用 connect 方式,每次发送报文都会需要这样的过程:
连接套接字→发送报文→断开套接字→连接套接字→发送报文→断开套接字 →………

而如果使用 connect 方式,就会变成下面这样:
连接套接字→发送报文→发送报文→……→最后断开套接字

我们知道,连接套接字是需要一定开销的,比如需要查找路由表信息。所以,UDP 客户端程序通过 connect 可以获得一定的性能提升。


参考:极客时间 - 网络编程实战(https://time.geekbang.org/column/article/129807)

EOF

阅读全文
回显程序

基础铺垫

首先来简单说几个问题。

1、程序双击后发生了什么?
程序双击后,程序会被操作系统装载入内存,包括程序的代码、数据等信息。然后从程序的入口开始执行。

2、本机的不同程序间可以相互对话(通信)吗?
当然是可以的,召唤师们经常用的 WeGame 和 LOL 是两个程序,当你在 WeGame 登录后点击启动 LOL,通常情况下再不用在 LOL 输入账号密码了,说明你再 WeGame 登录的账号信息也给了 LOL 的程序。至于是如何给的?
举个实现的例子:当你登录 WeGame 后,你会获得一个 ticket,这个 ticket 代表你的小票,这个小票就有着你的身份信息,你可以凭着这个小票进入召唤师峡谷。这个还得看程序员的具体的实现。

这个就属于进程间通信

3、如何实现不同主机间的不同进程的对话(通信)?
当然可以,你电脑上的 QQ 和室友电脑上的 QQ,双击后会被载入自己的内存,那么你发的话是如何被室友收到的呢?这个就是不同主机间不同进程的通信,利用网络通信!

4、那问题 3 中的两个 QQ 进程是如何找到对方的呢?
举个栗子:假设你有个女朋友,你的女朋友在北京上大学,你在西安上大学,你的女朋友给你准备了一份礼物,她要把这个礼物寄给你,她会填写快递信息如:
【中国陕西省西安市xxx区,西安xxx大学,xxx收,手机号xxx】
快递小哥可以根据这个地址和手机号唯一的定位到你,然后把快递送到你手里。

在计算机中也是有唯一标识的,这个东西就是 ==IP== 地址,IP 地址标识了网络上的一台主机。
IP 地址就相当于【中国陕西省西安市xxx区,西安xxx大学】。
但是你的电脑上正在运行着很多程序,QQ、LOL、WeGame 等等,对方怎么知道这个包裹发给哪个程序呢?这个就需要另一个东西叫==端口号==。这个端口号就唯一的标识了你电脑上的一个程序。
端口号就相当于【xxx收,手机号xxx】。

这样就可以唯一的标识一个网络上一个主机的一个进程了。

5、什么是协议?
协议就是一种约定,用 4 中的栗子来说,你的女朋友给你寄的是特产烤鸭,她打电话给你说让你拿到后热一下再吃,这就是一种协议。
当然你也可以拿到后不热直接吃。(别吃坏肚子了)
当然她使用中文填写的快递地址信息也是和快递小哥的一种协议(约定)。

6、计算机有大端和小端之分,那数据在网络上传输的时候是按照什么方式传输呢?
规定网络传输的数据都以大端序传输,即先发出的数据是低地址,后发出的数据是高地址。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节;
不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据;
如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,下文的代码实现均会做网络字节序和主机字节序的转换。
转换需要用到的函数:

1
2
3
4
5
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to network long
uint16_t htons(uint16_t hostshort); // host to network short
uint32_t ntohl(uint32_t netlong); // network to host long
uint16_t ntohs(uint16_t netshort); // network to host short

好了铺垫就到这里,来说说 UDP 协议。

UDP 协议

UDP 协议就是一种约定,约定了“快递小哥”拿到包裹后根据什么找到你。

UDP 协议格式如下:
在这里插入图片描述
其中需要注意的是:
1、2 个字节的 UDP 长度,是这整个 UDP 协议的长度,也就是说上面这张图的数据部分最多放 65535 - 64 字节的数据。资料:TCP、UDP数据包大小的限制
2、校验和,如果校验和出错(和实际收到的数据校验和对不上),这个包裹就被丢弃了。

UDP 网络回显程序

实现如下功能:
客户端 — hello —> 服务器
客户端 <— hello — 服务器

回显服务器测试

服务端实现

  1. 创建一个 socket;
  2. 服务器需要绑定一个固定的 IP 和端口号方便客户端找到他;
  3. 服务器启动并监听这个端口号;
  4. 循环处理:收到请求、处理请求、返回响应。
1
gcc echo_server.c -o echo_server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// echo_server.c
// UDP 回显服务器

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
// 创建 socket
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
// 绑定 IP 和 port
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
addr.sin_port = htons(8080);
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if (ret < 0) {
perror("bind");
return 1;
}
printf("server start!\n");

// 循环处理收到的请求、返回响应
while (1) {
// 读取请求
char buf[1024] = {0};
struct sockaddr_in client_addr; // 客户端 ip 和 port
socklen_t len = sizeof(client_addr);

ssize_t n = recvfrom(fd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&client_addr, &len);
if (n < 0) {
perror("recvfrom");
continue; // 当一次读取失败,服务器不退出
}
buf[n] = '\0';

// 处理请求
// 因为是回显服务器,原封不动返回给客户端

// 返回响应
n = sendto(fd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, len);
if (n < 0) {
perror("sendto");
continue;
}

printf("[%s:%d] buf: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);
}

close(fd);
return 0;
}

客户端实现

  1. 创建 socket;
  2. 配置要连接服务器的 ip 和 port;
  3. 循环处理:获取输入、发送给服务器、获取响应结果。
1
gcc echo_client.c -o echo_client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// echo_client.c
// UDP 回显客户端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
// 创建 socket
int fd = socket(AF_INET6, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}

// 配置要连接服务器的 ip 和 port
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8080);

while (1) {
// 获取输入
printf("input> ");
char input[1024] = {0};
scanf("%s", &input);

// 将输入发送给服务器
sendto(fd, input, sizeof(input), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
printf("req: %s\n", input);

// 接收响应
char resp[1024] = {0};
recvfrom(fd, resp, sizeof(resp), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
printf("resp: %s\n", resp);
}

close(fd);

return 0;
}

实现说明

1、socket()

socket 函数用来创建一个建立网络通信的端点。

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数:
domain,在上面的代码中使用 AF_NET,即 IPv4,当然还有其他的选项:

1
2
3
4
5
6
7
8
9
10
11
Name                Purpose                          Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)

type,套接字具有指定的类型,该类型指定通信语义。即代表协议,上述代码中使用 SOCK_DGRAM 代表 UDP。

1
2
SOCK_STREAM 	TCP协议使用这个
SOCK_DGRAM UDP协议

protocol,用来指定 socket 所使用的传输协议编号,通常为0。

返回值:
失败返回 -1,成功返回对应的文件描述符。

2、bind()
当使用 socket() 来创建一个 socket 时候,它存在于名称空间(地址族)中,但没有分配给它地址。
bind 用来给 socket 绑定 ip 和 端口。

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

返回值:
成功返回 0,失败返回 -1。
参数:
sockfd,创建的 socket 文件描述符。
addr,这个结构体存储协议族、ip、port等信息。
addr_len,addr 的大小。

1
2
3
4
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}

socket API 可以都用 struct sockaddr * 类型表示, 在使用的时候需要强制转化成 sockaddr_in;
这样的好处是程序的通用性,可以接收IPv4、IPv6, 以及UNIX Domain Socket 各种类型的sockaddr结构体指针做为参数;
在这里插入图片描述

3、sendto()

1
2
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

发送 buf 中的数据 len 个给 dest_addr。

4、recvfrom()

1
2
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

接收到一个数据,src_addr 是输出型参数,代表谁发给我的。

封装 UDP socket

可以看到,上面服务端、客户端实现时候,有一些同样的操作要做,那为了后面使用方便,封装一个 UDP 的 socket 类来实现这些共同的操作。

来分析一下需求:
客户端程序需要:

  • 创建 socket
  • 关闭 socket
  • 接收数据
  • 发送数据

服务端程序需要:

  • 创建 socket
  • 关闭 socket
  • 接收数据
  • 发送数据
  • 绑定端口号

实现

udp_socket.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#ifndef __UDP_SOCKET__
#define __UDP_SOCKET__

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <string>

class UdpSocket {
public:
UdpSocket() : m_fd(-1) {}
~UdpSocket() {
Close();
}

// 创建一个 UDP socket
// 成功返回 true,失败返回 false
bool Create() {
m_fd = socket(AF_INET, SOCK_DGRAM, 0);
return m_fd == -1 ? false : true;
}
// 关闭 socket
// 成功返回 true,失败返回 false
bool Close() {
return close(m_fd) == -1 ? false : true;
}
// 绑定 ip 和 port
bool Bind(const std::string& ip, const std::uint16_t& port) {
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
return bind(m_fd, (sockaddr*)&addr, sizeof(addr)) == -1 ? false : true;
}
// 接收数据
bool RecvFrom(std::string* msg, std::string* ip=nullptr, std::uint16_t* port=nullptr) {
char buf[1024] = { 0 };
sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
ssize_t n = recvfrom(m_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&peer, &peer_len);
if (n < 0) {
return false;
}
*msg = buf;
if (ip != nullptr) {
*ip = inet_ntoa(peer.sin_addr);
}
if (port != nullptr) {
*port = ntohs(peer.sin_port);
}
return true;
}
// 发送数据
bool SendTo(const std::string& msg, const std::string& ip, const std::uint16_t& port) {
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
ssize_t n = sendto(m_fd, msg.c_str(), msg.size(), 0, (sockaddr*)&addr, sizeof(addr));
return n == -1 ? false : true;
}

private:
int m_fd;
};

#endif // __UDP_SOCKET__

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 回显客户端
#include <iostream>

#include "udp_socket.hpp"

int main() {
UdpSocket socket;
bool ret = socket.Create();
if (!ret) {
perror("Socket::Create()");
return -1;
}

while (true) {
std::string input;
std::cout << "input> ";
std::cin >> input;

ret = socket.SendTo(input, "127.0.0.1", 8080);
if (!ret) {
perror("Socket::SendTo()");
continue;
}

std::string resp;
ret = socket.RecvFrom(&resp);
if (!ret) {
perror("Socket::RecvFrom()");
continue;
}
}

socket.Close();
return 0;
}

测试结果:OK
在这里插入图片描述


EOF

阅读全文
在 Linux 上编写和调试多线程程序

本文的环境:
Linux centos-7.shared 3.10.0-693.5.2.el7.x86_64 #1 SMP Fri Oct 20 20:32:50 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
gcc 版本 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)

本文使用 POSIX 线程库,需引入头文件 pthread.h,在编译的时候要注意添加 -lpthread 参数。

POSIX 是一个标准,约定一个操作系统应该提供哪些接口,pthread 即为 posix thread。C++11、Python、Java 均内置了线程库。

线程创建

1
2
3
4
5
6
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg
);

参数:
thread 是一个输出型参数,返回一个线程 id。
attr 可以设置线程属性,填 NULL 表示使用默认属性。
start_routine 是一个函数指针,是线程执行的入口函数。
arg 是 start_routine 的函数参数。
值得注意的是这个函数 arg 是不支持传递多个参数的(可变参数),如果需要传递多个函数就需要使用 struct 或者一些别的方式。

返回值:
成功返回 0,失败返回错误码。

栗子:
创建一个线程,传递一个参数,并在这个新线程内打印这个参数。
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

struct ThreadArg {
int num;
};

void* ThreadEntry(void* arg) {
while (1) {
printf("In ThreadEntry, %lu, arg %d\n", pthread_self(), ((struct ThreadArg*) arg)->num);
sleep(1);
}
}

int main() {
pthread_t tid;
struct ThreadArg ta;
ta.num = 20;
pthread_create(&tid, NULL, ThreadEntry, &ta);
while (1) {
printf("In Main Thread, %lu\n", pthread_self());
sleep(1);
}

return 0;
}

==注意:==
上面代码中使用了 pthread_self() 获取线程的 id,这是 POSIX 线程库提供的库函数,操作系统也提供了一个获取线程 id 的系统调用 gettid()

1
2
pthread_t pthread_self(void);
pid_t gettid(void);

但是当你在同一个线程调用两个函数的时候发现返回的并不是相同的值。

The thread ID returned by this call is not the same thing as a POSIX thread ID.
(i.e., the opaque value returned by pthread_self(3)).
对于单线程的进程,内核中tid==pid,对于多线程进程,他们有相同的pid,不同的tid。tid用于描述内核真实的pid和tid信息。
pthread_self返回的是posix定义的线程ID,man手册明确说明了和内核线程tid不同。它只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id。

gettid() 和 pthread_self()的区别

线程终止

想让一个线程结束而不终止进程:
1、从线程处理函数 return
2、线程自己调用 pthread_exit 切腹自尽。

1
void pthread_exit(void *retval);

参数:输入 & 输出型参数。
==注意==:pthread_exit 参数所指向的内存单元必须是全局的或者是用malloc 分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

3、兄弟线程调用 pthread_cancel 终止同一进程中的另一个线程。

1
int pthread_cancel(pthread_t thread);

参数:线程 id
返回值:成功返回 0,失败返回错误码。
==注意==:这个 pthread_cancel 是温和的终止一个线程而不是强制 kill 掉,抽象一个例子就是你在召唤师峡谷杀敌ing,你妈喊你吃饭,你可能得等一会才过去吃饭。

线程等待

为什么要线程等待?
例如,计算一个很大的矩阵相乘,可以使用多线程方式来计算,每个线程计算其中的一部分,最终等待所有的线程执行完,主线程汇总结果,这里就用 pthread_join 来保证逻辑。

1
int pthread_join(pthread_t thread, void **retval);

参数:thread 线程 id,retval 指向一个指针,这个指针指向线程的返回值,不关注线程的返回值可以填 NULL。
返回值:成功返回 0,失败返回错误码。
调用该函数的线程将挂起等待,直到 id 为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到 的终止状态是不同的:
1、如果thread线程通过 return 返回, value_ ptr 所指向的单元里存放的是thread线程函数的返回值。
2、如果thread线程被别的线程调用 pthread_cancel 异常终掉, value_ ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。
3、如果thread线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。

线程分离

类似于忽略 SIGCHLD 信号,分离后就代表线程的死活不管了,也就不用 pthread_join 回收了。

1
int pthread_detach(pthread_t thread);

可以自己把自己分离出去,也可以被兄弟线程分离。
pthread_detach(pthread_self());

使用 gdb 调试多线程程序

0、使用 gdb 调试多线程程序

使用 gdb 调试一个程序需要在编译时候加上 ==-g== 选项,为什么?百度去。

1
gdb attach 28966
1
2
3
info thread # 查看所有线程信息
bt # 查看当前线程调用栈
thread 2 # 切换当前线程

在这里插入图片描述

1、查看一个程序的所有线程信息

1
ps -eLf | grep a.out

参数:-L 表示 LWP,这里 ps 得到的线程 id 是和 gettid 一样的。
在这里插入图片描述

2、查看一个程序依赖哪些库

1
ldd a.out

在这里插入图片描述
3、查看一个进程中有几个线程和线程调用栈

1
pstack 27779

在这里插入图片描述


EOF

阅读全文