Skip to content

计算机网络的期末Project,用Python编写的聊天室

Notifications You must be signed in to change notification settings

KevinWang15/network-pj-chatroom

Repository files navigation

功能

  • Diffie-Hellman 密钥交换安全传输(启动时第一件事)
  • 用AES加密所有的传输内容
  • 包分为OpCode和Parameters,用binary序列化反序列化Parameters
  • 使用数据库存储用户信息、好友关系、房间信息、加入房间状态、所有聊天记录
  • tkinter GUI
  • 有新消息时自动滚动到底部
  • 窗口放大缩小
  • 服务器、客户端通过JSON文件配置
  • 联系人列表;按照最后发消息的时间排序好友和群;未读的消息用红点标注数量;显示并实时更新在线状态
  • 加好友功能,对方收到通知,通过/拒绝/推迟到下次登入时询问
  • 防止重复打开窗口,如果已经打开则使窗口获得焦点
  • 账号只能在一处登入,在别处登入时把原来登入的踢下线
  • 再次打开时恢复所有聊天记录、用户离线时收到的未读的消息用红点标注数量
  • 支持多行内容(Enter换行,Ctrl+Enter发送);支持聊天字体的设置
  • 支持图片传输
  • 群聊功能、加群、创建群
  • 群聊中显示群成员(双击打开聊天窗口/发送好友请求);实时显示群成员在线状态,置顶在线的成员
  • 智能的联系人列表中的最近消息(同QQ,群聊则显示发送者昵称,图片则显示[图片消息],自动处理换行)

安装说明

Python版本: 3.5

pip install pycrypto # 用于AES加密
pip install Pillow # 支持JPG等格式图片的发送

也可以运行

pip install -r requirements.txt

运行方法

python run_client.py
python run_server.py

(一次只能运行一个server,但可以运行N个client)

配置文件

server和client共用config.json

{
  "crypto": {
    "base": ..,
    "modulus": ...
  },
  "client": {
    "server_ip": "127.0.0.1",
    "server_port": 8111
  },
  "server": {
    "bind_ip": "0.0.0.0",
    "bind_port": 8111
  }
}

大多都容易理解,需要注意的是crypto部分,这里的basemodulus是Diffie-Hellman密钥交换时用到的,应该为两个大素数。

文件目录

│  config.json
│  README.md
│  run_client.py
│  run_server.py
│
├─client
│  │  __init__.py
│  │
│  ├─components
│  │      contact_item.py
│  │      vertical_scrolled_frame.py
│  │      __init__.py
│  │
│  ├─forms
│  │      boilerplate.txt
│  │      chat_form.py
│  │      contacts_form.py
│  │      login_form.py
│  │      register_form.py
│  │      __init__.py
│  │
│  ├─memory
│  │      __init__.py
│  │
│  └─util
│      │  __init__.py
│      │
│      └─socket_listener
│              __init__.py
│
├─common
│  │  config.py
│  │  global_vars.py
│  │  __init__.py
│  │
│  ├─cryptography
│  │      crypt.py
│  │      prime.py
│  │      __init__.py
│  │
│  ├─message
│  │      __init__.py
│  │
│  ├─transmission
│  │      secure_channel.py
│  │      __init__.py
│  │
│  └─util
│          __init__.py
│
└─server
    │  database.db
    │  main.sql
    │  __init__.py
    │
    ├─broadcast
    │      __init__.py
    │
    ├─event_handler
    │      add_friend.py
    │      client_echo.py
    │      create_room.py
    │      join_room.py
    │      login.py
    │      query_room_users.py
    │      register.py
    │      resolve_friend_request.py
    │      send_message.py
    │      __init__.py
    │
    ├─memory
    │      __init__.py
    │
    └─util
        │  __init__.py
        │
        └─database
                __init__.py

预览图

1 2 3 4 5 6 7 8

自制聊天协议说明

本聊天协议受到HTTP协议启发

  1. 无状态(除了登入操作之外)。
  2. Request Body和Response Body使用键值对的方式,键为string,值可以为任意类型。

略作了修改

  1. 登入时设置socket状态,后续传输不必再次说明身份。
  2. 每个包头加入Request/Response Type,每个Type对应一种固定的操作,对应固定的参数列表。
  3. 使用二进制而非类JSON方式传输,节省流量。

这种设计的优点

  1. 简单易懂,易于测试(因为无状态)。
  2. 灵活方便,无需为不同的操作类型写不同的处理程序。
  3. 可以传输非常复杂的数据结构(如:登入时的所有N条未读消息(每条都含有发件人/时间/消息内容/甚至图片二进制)都是放在一个包里的)。

这种设计的缺点

  1. 每个字段要指定一个key(如:user,pwd),每个value的类型要有一个byte指定,略浪费流量。

增加安全性,防止被窃听:

  1. 自制的安全传输协议,在启动时会使用Diffie-Hellman算法与服务器交换密钥。
  2. 交换完密钥后,所有的传输都会经过AES加密。
  3. 客户端和服务器除了在协议上需要达到共识,不需要共享任何其他数据。
  4. 在不安全的信道上,就算所有数据都被窃听,窃听者也无法解密消息内容。

缺点:

  1. 无法防中间人攻击(没有做数字签名等)。

8

协议详细说明

注:具体实现可以看src/common/cryptographysrc/common/transmissionsrc/common/message

协议设计的宗旨是:无状态、分层设计、以先发送长度再发送内容的方式设计。

网络包分为四层,前三层格式一致,第四层是值层,格式根据值类型不同而不同。构造分别如下:

层1(解密前):

  1. Length of Message Body (4Bytes)
  2. Length of AES padding (1Byte) AES加密填充用
  3. AES IV (16Bytes) AES初始向量
  4. Message Body 被AES加密的内容

层2(解密后):

  1. Message Type (4Bytes) 定义MessageType,见src/common/message
  2. Array of parameters.. N个层3的数据

层3(参数):

  1. Type of Parameter (1Byte) 定义VAR_TYPE,见src/common/message
  2. Length of Body (4 Bytes)
  3. Body of Parameter 具体层4的数据,怎么解释需要根据VAR_TYPE决定,见层4

层4(基础类型如int、str、bool、float、binary等):

  1. Packed Data (Using struct.pack in Python) (直接转成bytes)

层4(List):

  1. Array of Level 3 Data Pack (N个层3的数据)

层4(Dictionary):

  1. Length of Key (1 Byte) Key的长度
  2. Key Key的值(字符串)
  3. Level 3 Data Pack 层3数据作为Value的值
  4. Repeat.. 重复123

一般来说,真正使用时,会选择直接发送一个List或者Dictionary作为层3,这样可以一次和服务器交换多个参数。

比如self.sc.send(MessageType.login, [username, password])

这种设计非常强大,结合src/common/transmission中的AES加密,可以无缝和服务器安全交换非常复杂的数据。比如,一个用户的所有离线未读消息(有N条,每条可能是不同的房间、不用的发送者,都包含不同的时间、字体大小,可能还有图片),可以通过一个包发送给客户端,效率非常高。

用SQLite存储用户信息和未读消息

数据库在src/server/database.db,结构如下:

CREATE TABLE "chat_history" (
"id"  INTEGER NOT NULL,
"user_id"  INTEGER,
"target_id"  INTEGER,
"target_type"  TEXT,
"data"  BLOB,
"sent"  INTEGER,
PRIMARY KEY ("id" ASC)
);

CREATE TABLE "friends" (
"from_user_id"  INTEGER NOT NULL,
"to_user_id"  INTEGER NOT NULL,
"accepted"  TEXT,
PRIMARY KEY ("from_user_id" ASC, "to_user_id")
);

CREATE TABLE "rooms" (
"id"  INTEGER NOT NULL,
"room_name"  TEXT,
PRIMARY KEY ("id")
);

CREATE TABLE "room_user" (
"id"  INTEGER NOT NULL,
"room_id"  INTEGER,
"user_id"  INTEGER,
PRIMARY KEY ("id")
);

CREATE TABLE "users" (
"id"  INTEGER NOT NULL,
"username"  TEXT,
"password"  TEXT,
"nickname"  TEXT,
PRIMARY KEY ("id" ASC)
);

比较有趣的是消息记录,这里直接用BLOB层2的消息存下来。

现在我做的是消息记录全保存,就直接在服务器处理消息之前,把消息写入数据库,如果用户在线,sent设置为True,否则为False

如果不希望保存所有消息,只希望保存未读消息,那么就去掉sent属性,只在用户不在线时写数据库,在用户在线后把数据读出来、发给用户然后删除即可。

两个有启发性的问题

大图传输失败?(数据包20KB以下不存在此问题)

data = socket.recv(length_of_message)

第一感觉:接收length_of_message个字节,收满才继续执行程序。

真正含义:缓存区大小为length_of_message,接收一次数据!(缓存区满会继续,但继续不代表收满)

TCP只保证数据到达先后顺序/不会漏也不会发重,但是不保证每次会到达多少数据!

解决方法:

一定要在最先4个Bytes告知消息长度,然后用:

while(bytes_received<bytes_total):
	data=socket.recv(bytes_total-bytes_received)
	bytes_received+=len(data)
	buffer+=data

对于select等非阻塞socket这个问题更加复杂一些,不过原理是相同的。

Windows和Linux之间互发消息,消息无法解读。

Python在不同系统下long的长度不一样。

Python内存中字节序也不一样。

解决方法:

在打包时统一转换为“network byte order”,即big endian,long统一为32位。(可表示4GB长度,够用)

解码时解成自己系统相对应的,放入内存中。

About

计算机网络的期末Project,用Python编写的聊天室

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages