mirror of
https://github.com/dongdigua/dongdigua.github.io
synced 2024-11-24 04:03:11 +08:00
快完事了, 预览测试
This commit is contained in:
parent
2dfc5097a9
commit
9822a9cfaa
2
index.md
2
index.md
@ -1,2 +1,2 @@
|
|||||||
## dongdigua's blog
|
## dongdigua's blog
|
||||||
todo
|
[elixir写点对点加密聊天软件](https://dongdigua.github.io/p2p_chat)
|
||||||
|
BIN
miku_sheep.jpg
Normal file
BIN
miku_sheep.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 282 KiB |
287
p2p_chat.md
287
p2p_chat.md
@ -1,44 +1,49 @@
|
|||||||
# 讲一讲我前段时间做的一个点对点加密聊天软件
|
# 讲一讲我前段时间做的一个点对点加密聊天软件
|
||||||
|
![miku](https://dongdigua.github.io/miku_sheep.jpg)
|
||||||
## 版权声明
|
## 版权声明
|
||||||
本文章**仅在**本人github pages发布,
|
本文章**仅在**本人github pages发布,<br>
|
||||||
转载给爷**标上源链接**, 敢在某些平台转载投自制的**你\*\**!**
|
转载给爷**标上源链接**, 敢在某些平台转载投自制的**你\*\**!**<br>
|
||||||
CC BY-NC-SA
|
CC BY-NC-SA
|
||||||
|
|
||||||
|
## 开头
|
||||||
|
项目地址: https://github.com/dongdigua/p2p_chat<br>
|
||||||
|
写着玩的小项目, 肯定有很多不足
|
||||||
|
|
||||||
## 使用到的技术
|
## 使用到的技术
|
||||||
- UDP socket
|
- UDP socket
|
||||||
- 进程(这里指beam虚拟机的进程)
|
- 进程(这里指beam虚拟机的进程)
|
||||||
- GenServer
|
- GenServer
|
||||||
- ETS键值存储(Erlang Term Storage)(类似redis, 也是在内存中的)
|
- ETS键值存储(Erlang Term Storage)[ETS教程](https://elixirschool.com/zh-hans/lessons/storage/ets)
|
||||||
- escript编译成可执行文件
|
- escript编译成可执行文件
|
||||||
- rsa非对称加密
|
- rsa非对称加密
|
||||||
|
|
||||||
## 大体架构
|
## 大体架构
|
||||||
整个思路来源都是从这两个视频来的, UDP打洞
|
整个思路来源都是从这两个视频来的, UDP打洞<br>
|
||||||
[with netcat](https://www.youtube.com/watch?v=s_-UCmuiYW8)[with python](https://www.youtube.com/watch?v=IbzGL_tjmv4)
|
[with netcat](https://www.youtube.com/watch?v=s_-UCmuiYW8) & [with python](https://www.youtube.com/watch?v=IbzGL_tjmv4)<br>
|
||||||
我的理解就是通过发送UDP包打开一个端口来让远程电脑能知道你的端口映射到了公网IP的哪个端口,
|
我的理解就是通过发送UDP包打开一个端口来让远程电脑能知道你的端口映射到了公网IP的哪个端口,<br>
|
||||||
然后将两个需要发消息的客户端相互告诉对方各自的公网IP以及映射到的端口, 就能实现p2p通信.
|
然后将两个需要发消息的客户端相互告诉对方各自的公网IP以及映射到的端口, 就能实现p2p通信.<br>
|
||||||
|
|
||||||
客户端使用GenServer来实现后端接口和网络通信, 在CLI模块处理用户输入调用GenServer.
|
客户端使用GenServer来实现后端接口和网络通信, 在CLI模块处理用户输入调用GenServer.<br>
|
||||||
服务端可以很简单, 就是收到两个IP然后相互发送对方的地址让客户端能够相互通信,
|
|
||||||
但是为了能够接受多对客户端以及非阻塞等待客户端, 就用ETS存储客户端的信息,
|
服务端可以很简单, 就是收到两个IP然后相互发送对方的地址让客户端能够相互通信,<br>
|
||||||
为了让客户端不乱配对, 就需要增加一个注册功能, 也使用ETS实现.
|
但是为了能够接受多对客户端以及非阻塞等待客户端, 就用ETS存储客户端的信息,<br>
|
||||||
|
为了让客户端不乱配对, 就需要增加一个注册功能, 也使用ETS实现.<br>
|
||||||
|
|
||||||
## 客户端实现
|
## 客户端实现
|
||||||
内容比较多, 所以我不会讲的很全, 代码不会都放出来
|
内容比较多, 所以我不会讲的很全, 代码不会都放出来
|
||||||
项目目录大概是这样
|
项目目录大概是这样
|
||||||
```sh
|
```sh
|
||||||
├── client
|
├── client
|
||||||
│ ├── lib
|
│ ├── lib
|
||||||
│ │ ├── client
|
│ │ ├── client
|
||||||
│ │ │ ├── cli.ex # 和用户交互, 调用GenServer后端, escript入口点
|
│ │ │ ├── cli.ex # 和用户交互, 调用GenServer后端, escript入口点
|
||||||
│ │ │ ├── connect.ex # 处理与服务器发送和接受的二进制字符串
|
│ │ │ ├── connect.ex # 处理与服务器发送和接受的二进制字符串
|
||||||
│ │ │ ├── crypto.ex # rsa加密解密
|
│ │ │ ├── crypto.ex # rsa加密解密
|
||||||
│ │ │ └── register.ex # 仅生成注册时需要发送的二进制字符串
|
│ │ │ └── register.ex # 仅生成注册时需要发送的二进制字符串
|
||||||
│ │ └── client.ex # 客户端核心程序, 包含GenServer和socket通信
|
│ │ └── client.ex # 客户端核心程序, 包含GenServer和socket通信
|
||||||
│ ├── mix.exs
|
│ ├── mix.exs
|
||||||
│ └── mix.lock
|
│ └── mix.lock
|
||||||
```
|
```
|
||||||
|
|
||||||
注意这里只在核心程序处理socket, cli模块处理用户交互, 使项目分层化
|
注意这里只在核心程序处理socket, cli模块处理用户交互, 使项目分层化
|
||||||
|
|
||||||
### escript
|
### escript
|
||||||
@ -76,6 +81,81 @@ end
|
|||||||
main\_module指定了程序的入口点main函数, extra\_applications加入erlang库:crypto因为后续需要使用加密
|
main\_module指定了程序的入口点main函数, extra\_applications加入erlang库:crypto因为后续需要使用加密
|
||||||
|
|
||||||
### GenServer和socket
|
### GenServer和socket
|
||||||
|
先是定义了两个结构体, 一个用于存储peer的信息, 一个存储客户端的信息(peer键是peer结构体)<br>
|
||||||
|
然后是一堆常量, 服务器可以改成你的128核心1TB内存1EB固态硬盘的小型服务器的地址, key\_integer是客户端的密钥生成器用的<br>
|
||||||
|
其实可以在config.exs或者用json来配置, 但是我懒哈哈
|
||||||
|
```elixir
|
||||||
|
defmodule Client do
|
||||||
|
defmodule Peer do
|
||||||
|
defstruct [:name, :addr, :port, :pub_key]
|
||||||
|
end
|
||||||
|
@serveraddr {127, 0, 0, 1}
|
||||||
|
@serverpt 1234
|
||||||
|
@key_integer <<3>> #should be a valid rsa key_integer
|
||||||
|
use GenServer
|
||||||
|
alias Client.Conn
|
||||||
|
defstruct [:socket, :name, :priv_key, peer: nil]
|
||||||
|
```
|
||||||
|
|
||||||
|
GenServer初始化, UDP打开一个端口(从命令行参数传进来)
|
||||||
|
```elixir
|
||||||
|
def start_link(port) do
|
||||||
|
{:ok, socket} = :gen_udp.open(port, [:binary, active: false])
|
||||||
|
GenServer.start_link(Client, %Client{socket: socket}, name: :client)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
然后是GenServer的回调函数, 实现了必要的接口
|
||||||
|
```elixir
|
||||||
|
def init(%Client{} = client), do: {:ok, client}
|
||||||
|
|
||||||
|
def handle_call({:register, sesstoken, passwd}, _from, client) do
|
||||||
|
:ok = :gen_udp.send(client.socket, @serveraddr, @serverpt, Client.Reg.register(sesstoken, passwd))
|
||||||
|
{:reply, :gen_udp.recv(client.socket, 0), client}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:find, name, sesstoken, passwd}, _from, client) do
|
||||||
|
:ok = :gen_udp.send(client.socket, @serveraddr, @serverpt, Conn.find_peer(name, sesstoken, passwd))
|
||||||
|
case :gen_udp.recv(client.socket, 0) |> Conn.parse_peer() do
|
||||||
|
{:ok, peer} ->
|
||||||
|
{:reply, peer, %{client | name: name, peer: peer}}
|
||||||
|
{:error, reason} ->
|
||||||
|
{:reply, reason, client}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call(:key, _from, client) do
|
||||||
|
{pub, priv} = Client.Crypto.generate_key(@key_integer)
|
||||||
|
:ok = :gen_udp.send(client.socket, client.peer.addr, client.peer.port, hd(tl(pub)))
|
||||||
|
{:ok, {_addr, _port, peer_pub}} = :gen_udp.recv(client.socket, 0)
|
||||||
|
full_peer_pub = [@key_integer, peer_pub]
|
||||||
|
{:reply, full_peer_pub,
|
||||||
|
%{client | priv_key: priv, peer: %{client.peer | pub_key: full_peer_pub}}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:chat, text}, _from, client) do
|
||||||
|
encrypted = Client.Crypto.encrypt(text, client.peer.pub_key)
|
||||||
|
:ok = :gen_udp.send(client.socket, client.peer.addr, client.peer.port, encrypted)
|
||||||
|
{:reply, encrypted, client}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_cast(:recv, client) do
|
||||||
|
spawn(fn -> recv_loop(client.socket, client.priv_key) end)
|
||||||
|
{:noreply, client}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
最后是接收消息(和解密)
|
||||||
|
```elixir
|
||||||
|
defp recv_loop(socket, priv_key) do
|
||||||
|
{:ok, {_ip, _port, data}} = :gen_udp.recv(socket, 0)
|
||||||
|
decrypted = Client.Crypto.decrypt(data, priv_key)
|
||||||
|
IO.puts(IO.ANSI.clear_line() <> "\r" <> IO.ANSI.cyan() <> "received: #{inspect(decrypted)}" <> IO.ANSI.reset())
|
||||||
|
recv_loop(socket, priv_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
### 用户交互CLI
|
### 用户交互CLI
|
||||||
首先使用OptionParser解析命令行参数, 如果解析成功就启动GenServer
|
首先使用OptionParser解析命令行参数, 如果解析成功就启动GenServer
|
||||||
@ -92,56 +172,163 @@ def main(args \\ []) do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
然后main\_cli()就处理用户的输入, 没什么好讲的.
|
然后main\_cli()就处理用户的输入,<br>
|
||||||
这里主要说一下输入密码的部分:
|
然后先向服务器发起find peer请求(需要身份验证), 找到peer之后交换密钥然后就可以发消息了<br>
|
||||||
|
这里主要说一下输入密码的部分:<br>
|
||||||
erlang的:io.get_password()函数在mix中不管用, 所以就需要自己写一个清空用户输入的小东西
|
erlang的:io.get_password()函数在mix中不管用, 所以就需要自己写一个清空用户输入的小东西
|
||||||
```elixir
|
```elixir
|
||||||
def gets_passwd(prompt) do
|
def gets_passwd(prompt) do
|
||||||
pid = spawn(fn -> clear_input(prompt) end)
|
pid = spawn(fn -> clear_input(prompt) end)
|
||||||
value = IO.gets("")
|
value = IO.gets("")
|
||||||
send(pid, :stop)
|
send(pid, :stop)
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_input(prompt) do
|
def clear_input(prompt) do
|
||||||
IO.write(IO.ANSI.clear_line() <> "\r" <> prompt) #\r用于回到行首
|
IO.write(IO.ANSI.clear_line() <> "\r" <> prompt) #\r用于回到行首
|
||||||
:timer.sleep(10)
|
:timer.sleep(10)
|
||||||
receive do
|
receive do
|
||||||
:stop -> IO.write("\r")
|
:stop -> IO.write("\r")
|
||||||
after
|
after
|
||||||
0 -> clear_input(prompt)
|
0 -> clear_input(prompt)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## 服务端实现
|
## 服务端实现
|
||||||
|
服务端仅作为暴露客户端连接和将两个客户端牵手的作用, 当然为了区分还要有注册功能<br>
|
||||||
|
项目目录大概是这样
|
||||||
|
```sh
|
||||||
|
└── server
|
||||||
|
├── Dockerfile
|
||||||
|
├── lib
|
||||||
|
│ ├── server
|
||||||
|
│ │ ├── application.ex
|
||||||
|
│ │ ├── connection.ex
|
||||||
|
│ │ └── register.ex
|
||||||
|
│ └── server.ex
|
||||||
|
└── mix.exs
|
||||||
|
```
|
||||||
|
这里用docker方便部署
|
||||||
|
### 应用程序监视器
|
||||||
|
因为是服务端嘛, 鬼知道用户或其它东西会整出什么么蛾子, 所以使用应用程序监视器在程序挂掉时重启进程很有必要<br>
|
||||||
|
这也是erlang/OTP的let it crash哲学的一种体现[我做的"crash辅导"视频](https://www.bilibili.com/video/BV193411A7fa)
|
||||||
|
```elixir
|
||||||
|
defmodule Server.Application do
|
||||||
|
use Application
|
||||||
|
@impl true
|
||||||
|
def start(_type, _args) do
|
||||||
|
children = [
|
||||||
|
%{
|
||||||
|
id: Server,
|
||||||
|
start: {Server, :start, []}
|
||||||
|
}]
|
||||||
|
opts = [strategy: :one_for_one, name: Server.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
### socket
|
### socket
|
||||||
### 存储
|
首先还是定义一个结构体用于存储每个用户的数据<br>
|
||||||
|
启动两个数据库, 打开UDP端口
|
||||||
|
```elixir
|
||||||
|
defmodule Server do
|
||||||
|
defmodule UserData do
|
||||||
|
defstruct [:addr, :port, :name, :sesstoken, :passwd]
|
||||||
|
end
|
||||||
|
import Server.Conn
|
||||||
|
@serverpt 1234
|
||||||
|
|
||||||
|
def start do
|
||||||
|
Server.Reg.new()
|
||||||
|
Server.Conn.table_new()
|
||||||
|
{:ok, socket} = :gen_udp.open(@serverpt, [:binary, active: false])
|
||||||
|
serve(socket)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
根据收到的消息头部的不同选择处理不同的内容(其实这里的区分判断应该写全, 但是我懒, 能用就行呗)<br>
|
||||||
|
然后递归调用自己形成循环(elixir有尾递归优化, 所以这样递归不会有性能问题)
|
||||||
|
```elixir
|
||||||
|
def serve(socket) do
|
||||||
|
case :gen_udp.recv(socket, 0) |> IO.inspect() do
|
||||||
|
{:ok, {_ip, _port, <<?F, _rest::binary>>} = data} ->
|
||||||
|
#FROM:foo;SESSTOKEN:test;PASSWD:hash
|
||||||
|
spawn(fn -> handle_connection(socket, data) end)
|
||||||
|
{:ok, {_ip, _port, <<?R, _rest::binary>>} = data} ->
|
||||||
|
#REGISTER:token:passwd
|
||||||
|
spawn(fn -> handle_register(socket, data) end)
|
||||||
|
{:error, error} ->
|
||||||
|
IO.inspect(error)
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
serve(socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_connection(socket, {ip, port, bin}) do
|
||||||
|
[_, name, sesstoken, passwd] = Regex.run(~r/FROM:(\w+);SESSTOKEN:(\w+);PASSWD:(\w+)/, bin)
|
||||||
|
user_data = %UserData{
|
||||||
|
addr: ip,
|
||||||
|
port: port,
|
||||||
|
name: name,
|
||||||
|
sesstoken: sesstoken,
|
||||||
|
passwd: passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
case find_peer(user_data) do
|
||||||
|
nil -> add_peer(user_data)
|
||||||
|
{{peer0, msg0}, {peer1, msg1}} ->
|
||||||
|
:gen_udp.send(socket, peer0, msg0)
|
||||||
|
:gen_udp.send(socket, peer1, msg1)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_register(socket, {ip, port, bin}) do
|
||||||
|
[_, token, passwd] = Regex.run(~r/REGISTER:(\w+):(\w+)/, bin)
|
||||||
|
if Server.Reg.register_session(token, passwd) do
|
||||||
|
:gen_udp.send(socket, ip, port, "successful!")
|
||||||
|
else
|
||||||
|
:gen_udp.send(socket, ip, port, "exists")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
### 客户端
|
||||||
|
### 服务端
|
||||||
|
|
||||||
|
|
||||||
## 后记
|
## 后记
|
||||||
### 分层实现
|
|
||||||
### elixir/erlang UI?
|
|
||||||
查了一下, elixir/erlang的基于文本界面(tui)的库好像都得调用C, 有点难受,
|
|
||||||
然后:gl官方文档看不懂...
|
|
||||||
### 感想
|
### 感想
|
||||||
|
#### 分层实现
|
||||||
|
#### 结构化数据
|
||||||
|
- markdown用VSCode真香!
|
||||||
|
### elixir/erlang UI?
|
||||||
|
查了一下, elixir/erlang的基于文本界面(tui)的库好像都得调用C, 有点难受,<br>
|
||||||
|
然后:gl官方文档看不懂...
|
||||||
### Rust杂谈
|
### Rust杂谈
|
||||||
正如上文(ui)所说, elixir实现ui总是要调用底层库, 然后OpenGL的支持也没有相关的教程(可能即使实现出来了渲染效率也不高).
|
正如上文(ui)所说, elixir实现ui总是要调用底层库, 然后OpenGL的支持也没有相关的教程(可能即使实现出来了渲染效率也不高).<br>
|
||||||
还有就是我想知道是elixir里面的一些数据结构如何存储的, 所以我最近准备学一学Rust这个比较底层的语言.
|
还有就是我想知道是elixir里面的一些数据结构如何存储的, 所以我最近准备学一学Rust这个比较底层的语言.<br>
|
||||||
现在刚开始学, 在看the book以及B站相关视频, 然后还是在exercism上做练习, Rust学起来有些地方和之前学elixir挺不同的:
|
现在刚开始学, 在看the book以及B站相关视频, 然后还是在exercism上做练习, Rust学起来有些地方和之前学elixir挺不同的:<br>
|
||||||
- 静态类型: 我之前一直写动态类型的语言(py, ex, jl), 静态有些不适应, 但其实还好
|
- 静态类型: 我之前一直写动态类型的语言(py, ex, jl), 静态有些不适应, 但其实还好
|
||||||
- 难: 很有挑战性, exercism上的learning exercises没有elixir那么详细
|
- 难: 很有挑战性, exercism上的learning exercises没有elixir那么详细
|
||||||
- 现代, 放心, 受限?: Rust编译器是真滴强大, 静态检查很多细节都能检查出来, 而且所有权让内存管理更安全了,
|
- 现代, 放心, 受限?: Rust编译器是真滴强大, 静态检查很多细节都能检查出来, 而且所有权让内存管理更安全了,<br>
|
||||||
不像C随便写一些就segfault, 也不用预先定义函数,
|
不像C随便写一些就segfault, 也不用预先定义函数
|
||||||
```C
|
```c
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
int ref();
|
int ref();
|
||||||
int main(int argc, char *argv){
|
int main(int argc, char *argv){
|
||||||
int *a = ref();
|
int *a = ref();
|
||||||
printf("%s", *a);
|
printf("%s", *a);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int ref(){
|
int ref(){
|
||||||
char *s = "haha";
|
char *s = "haha";
|
||||||
return &s;
|
return &s;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
但是有些地方(暂时这个水平)觉得有点受限, 没有C那么为所欲为, 但这一段我应该看完unsafe rust在下结论233
|
但是有些地方(暂时这个水平)觉得有点受限, 没有C那么为所欲为, 但这一段我应该看完unsafe rust在下结论233
|
||||||
|
Loading…
Reference in New Issue
Block a user