├── .gitignore ├── chat ├── __init__.py ├── my_client.py └── my_server.py ├── db ├── __init__.py ├── network.db └── user.py ├── settings.py └── transfer ├── FileTransfer.py ├── __init__.py ├── recv.txt └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | learn_net/ 107 | learn_threading/ 108 | .idea/ 109 | -------------------------------------------------------------------------------- /chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chevalx/PythonChat/c26fc837c2ecdd79094e8b8fa4bea4475d23752c/chat/__init__.py -------------------------------------------------------------------------------- /chat/my_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import json 3 | from settings import header_struct 4 | 5 | # header_struct 定义在 settings.py 里 6 | # struct 模块是一个 python 标准库 7 | # 可以将一部分变量(包括int, 字符串等)打包成类似于 C 语言里结构体一样的东西 8 | # 且长度固定,因此使得头部包长度固定。。。 9 | 10 | def recvall(sock, length): 11 | # 接收客户端发送的包,无论是只有长度的包(头部包)还是正式消息的包(正文) 12 | # 由于头部包本身长度固定,所以接收的时候传入固定长度的length即可 13 | # 而在收到头部包之后,根据头部包里的内容(指示了正文有多长)传入length以接收接下来的正文 14 | blocks = [] 15 | while length: 16 | block = sock.recv(length) 17 | if not block: 18 | raise EOFError('socket closed with {} bytes left' 19 | ' in this block'.format(length)) 20 | length -= len(block) 21 | blocks.append(block) 22 | return b''.join(blocks) 23 | 24 | 25 | def get_block(sock): 26 | # block 就是包的意思。。。 27 | # 可以是头部包,也可以是正文。。。 28 | data = recvall(sock, header_struct.size) 29 | block_length = header_struct.unpack(data)[0] # unpack返回tuple,所以要加索引 30 | return recvall(sock, block_length) 31 | 32 | 33 | def put_block(sock, content, info): 34 | # 发送包,先发头部包,再发正文包 35 | # 正文里有信息,固定为 100 位 36 | # 如果不够用空格补齐 37 | if len(info) <= 99: 38 | info += ' ' * (99 - len(info)) 39 | content = info + content 40 | block_length = len(content) 41 | sock.send(header_struct.pack(block_length)) 42 | sock.send(content.encode("utf-8")) 43 | 44 | 45 | class Menu: 46 | def __init__(self): 47 | self.logged = False 48 | self.choices = { 49 | "1": self.login, 50 | "2": self.register, 51 | "3": self.query, 52 | "4": self.chat_with_all, 53 | "5": self.chat_with_one 54 | } # 每个选项都对应了一个函数 55 | 56 | def display_menu(self): 57 | # 1,2,3三个操作均在输入数字后显示结果,并再次出现这个菜单 58 | # 4,5两个操作在输入某一字符才退出,不然就一直和别人聊天。。 59 | print(''' 60 | ----菜单 61 | 62 | --------1. 登录 63 | --------2. 注册 64 | --------3. 查询在线用户 65 | --------4. 群聊 66 | --------5. 与单一用户聊天 67 | ''') 68 | 69 | def login(self): 70 | # 如果已经登录过就不能再次登录,由logged标识。。 71 | if self.logged: 72 | return 73 | 74 | username = input("请输入用户名") 75 | password = input("请输入密码") 76 | # 正文 77 | login_info = username + " " + password 78 | # 正文包里的信息,指示服务器接下来要干啥。。当然服务器返回的包里也会有服务器定义的信息。。 79 | req_dict = { 80 | "command": 0, 81 | "from": username, 82 | "to": "server" 83 | } 84 | req_info = json.dumps(req_dict) 85 | 86 | # 发送包 87 | put_block(sock, login_info, req_info) 88 | 89 | # 接收包 90 | resp = get_block(sock).decode("utf-8") 91 | # 取出正文的信息 92 | info = json.loads(resp[:99].rstrip(" ")) 93 | cmd = info["command"] 94 | source = info["from"] 95 | to = info["to"] 96 | content = resp[99:] 97 | if content == "success": 98 | print("登录成功") 99 | self.logged = True 100 | return True 101 | print("登录失败") 102 | return False 103 | 104 | def query(self): 105 | pass 106 | 107 | def register(self): 108 | pass 109 | 110 | def chat_with_all(self): 111 | pass 112 | 113 | def chat_with_one(self): 114 | pass 115 | 116 | def run(self): 117 | while True: 118 | self.display_menu() 119 | choice = input("输入选项: ") 120 | action = self.choices.get(choice) 121 | if action: 122 | action() 123 | else: 124 | print("无效选项,请重新输入: ") 125 | 126 | 127 | if __name__ == '__main__': 128 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 129 | 130 | sock.connect(('localhost', 5550)) 131 | 132 | Menu().run() 133 | -------------------------------------------------------------------------------- /chat/my_server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | from db.user import Repository, User 4 | import json 5 | from settings import header_struct 6 | 7 | 8 | def recvall(sock, length): 9 | blocks = [] 10 | while length: 11 | block = sock.recv(length) 12 | if not block: 13 | raise EOFError('socket closed with {} bytes left' 14 | ' in this block'.format(length)) 15 | length -= len(block) 16 | blocks.append(block) 17 | return b''.join(blocks) 18 | 19 | 20 | def get_block(sock): 21 | data = recvall(sock, header_struct.size) 22 | block_length = header_struct.unpack(data)[0] 23 | return recvall(sock, block_length) 24 | 25 | 26 | def put_block(sock, content, info): 27 | if len(info) <= 99: 28 | info += ' ' * (99 - len(info)) 29 | content = info + content 30 | block_length = len(content) 31 | sock.send(header_struct.pack(block_length)) 32 | sock.send(content.encode("utf-8")) 33 | 34 | 35 | # 把whatToSay传给除了exceptNum的所有人 36 | def tellOthers(except_name, content): 37 | for conn, user in user_dict: 38 | if user != except_name: 39 | try: 40 | resp_dict = { 41 | "command": 5, 42 | "from": "server", 43 | "to": user 44 | } 45 | resp_info = json.dumps(resp_dict) 46 | put_block(conn, content, resp_info) 47 | except: 48 | pass 49 | 50 | 51 | # 登录 52 | def login(conn, info, repo): 53 | username, password = info.split(" ") 54 | user = User(username, password) 55 | 56 | # 构造响应 57 | resp_dict = { 58 | "command": 5, 59 | "from": "server", 60 | "to": username 61 | } 62 | resp_info = json.dumps(resp_dict) 63 | 64 | try: 65 | repo.login(user) 66 | conn_list.append(conn) 67 | if username in user_dict.values(): 68 | raise ValueError("该用户已登陆") 69 | user_dict[conn] = username 70 | resp_content = "success" 71 | put_block(conn, resp_content, resp_info) 72 | return True 73 | except ValueError as e: 74 | (resp_content, ) = e.args 75 | put_block(conn, resp_content, resp_info) 76 | return False 77 | 78 | 79 | # 输入要执行的操作 80 | # 解包全部在此完成 81 | def operation(conn): 82 | repo = Repository(db_file) 83 | while True: 84 | try: 85 | req = get_block(conn).decode("utf-8") 86 | info = json.loads(req[:99].rstrip(" ")) 87 | cmd = info["command"] 88 | source = info["from"] 89 | to = info["to"] 90 | content = req[99:] 91 | if cmd == 0: 92 | login(conn, content, repo) 93 | elif cmd == 1: 94 | # TODO 查询用户是否已登录 95 | pass 96 | elif cmd == 2: 97 | # TODO 查询用户是否已登录 98 | pass 99 | elif cmd == 3: 100 | pass 101 | elif cmd == 4: 102 | pass 103 | elif cmd == 5: 104 | pass 105 | 106 | except (OSError, ConnectionResetError): 107 | try: 108 | conn_list.remove(conn) 109 | except: 110 | pass 111 | print(user_dict[conn], '已经离开,还剩下', len(conn_list), '人') 112 | tellOthers(user_dict[conn], '【系统提示:' + user_dict[conn] + ' 离开聊天室】') 113 | conn.close() 114 | return 115 | 116 | 117 | if __name__ == '__main__': 118 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | sock.bind(('localhost', 5550)) 120 | sock.listen(5) 121 | print('服务器', socket.gethostbyname('localhost'), '正在监听 ...') 122 | 123 | # 这里与原来不同,去掉了conn_no, 字典里是 conn: username 124 | user_dict = dict() # conn: username 125 | # 列表里是conn,每个conn就是一个连接。。。 126 | conn_list = list() 127 | 128 | # 数据库文件 129 | db_file = "../db/network.db" 130 | 131 | while True: 132 | connection, addr = sock.accept() 133 | print('接受一个新的连接', connection.getsockname(), connection.fileno()) 134 | try: 135 | dialogue_thd = threading.Thread(target=operation, args=(connection,)) 136 | dialogue_thd.setDaemon(True) 137 | dialogue_thd.start() 138 | except: 139 | pass 140 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chevalx/PythonChat/c26fc837c2ecdd79094e8b8fa4bea4475d23752c/db/__init__.py -------------------------------------------------------------------------------- /db/network.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chevalx/PythonChat/c26fc837c2ecdd79094e8b8fa4bea4475d23752c/db/network.db -------------------------------------------------------------------------------- /db/user.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | 3 | 4 | class User: 5 | # TODO 给名字加上长度限制 6 | def __init__(self, name=None, password=None): 7 | self.name = name 8 | if len(password) < 6: 9 | raise ValueError("密码过短") 10 | self.password = password 11 | 12 | 13 | class Repository: 14 | def __init__(self, db_file): 15 | self.conn = sqlite3.connect(db_file) 16 | 17 | def select_others(self, names): 18 | cursor = self.conn.cursor() 19 | sql = "SELECT name from main.user where id in (%s)" % ','.join('?'*len(names)) 20 | cursor.execute(sql, names) 21 | 22 | def insert_user(self, user): 23 | cursor = self.conn.cursor() 24 | try: 25 | sql = "INSERT INTO main.user(name, password) VALUES (?, ?)" 26 | cursor.execute(sql, (user.name, user.password)) 27 | row_id = cursor.lastrowid 28 | except sqlite3.IntegrityError: 29 | self.conn.rollback() 30 | raise ValueError("用户名已存在") 31 | self.conn.commit() 32 | cursor.close() 33 | return row_id 34 | 35 | def select_user_by_name(self, name): 36 | # TODO 添加异常,没有该用户的情况 37 | cursor = self.conn.cursor() 38 | sql = "SELECT name, password from main.user where name=(?)" 39 | cursor.execute(sql, (name,)) 40 | row = cursor.fetchone() 41 | cursor.close() 42 | return row 43 | 44 | def login(self, user: User): 45 | row = self.select_user_by_name(user.name) 46 | if user.password != row[1]: 47 | raise ValueError("Password is not correct.") 48 | print(row) 49 | return 50 | 51 | def register(self, user: User): 52 | latest_id = self.insert_user(user) 53 | print(latest_id) 54 | return latest_id 55 | 56 | def close(self): 57 | self.conn.close() 58 | 59 | 60 | # 以下部分测试用 61 | if __name__ == '__main__': 62 | repo = Repository() 63 | testUser = User("test4", "12345678") 64 | repo.register(testUser) 65 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import json 3 | 4 | # 字段设计 5 | # 6 | # command: 7 | # login 0 8 | # register 1 9 | # query 2 10 | # chat 3 11 | # file 4 12 | # info 5 13 | # from: 14 | # server/username 15 | # to: 16 | # all/server/username 17 | 18 | block_info = { 19 | "command": 0, 20 | "from": "Chandler", 21 | "to": "Monica" 22 | } 23 | 24 | header_struct = struct.Struct('!I') 25 | 26 | if __name__ == '__main__': 27 | json_info = json.dumps(block_info) 28 | print(json_info) 29 | print(type(json.loads(json_info))) -------------------------------------------------------------------------------- /transfer/FileTransfer.py: -------------------------------------------------------------------------------- 1 | import socket # Import socket module 2 | import os 3 | import argparse 4 | 5 | # 传输文件的测试代码。。。 6 | # 很久之前写的,虽然能运行但跟不符合现在的代码结构,这一部分最后做吧。。。 7 | def recvall(sock): 8 | data = b'' 9 | while True: 10 | more = sock.recv(1024) 11 | data += more 12 | if len(more) < 1024: 13 | break 14 | return data 15 | 16 | 17 | def client(host, port): 18 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Create a socket object 19 | 20 | sock.connect((host, port)) 21 | 22 | path = r"C:\Users\zhoulinxuan\PycharmProjects\NetProg\transfer\test.txt" 23 | filename = os.path.basename(path) 24 | with open(filename, 'rb') as f: 25 | content = f.read() 26 | sock.sendall(content) 27 | print('Finish reading the file') 28 | 29 | f.close() 30 | print('Successfully send the file') 31 | sock.shutdown(socket.SHUT_WR) 32 | print('connection closed') 33 | 34 | 35 | def server(interface, port): 36 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 37 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 38 | sock.bind((interface, port)) 39 | sock.listen(1) 40 | print("listening at", sock.getsockname()) 41 | 42 | path = r"./recv.txt" 43 | 44 | while True: 45 | conn, addr = sock.accept() # Establish connection with client. 46 | print('Got connection from', addr) 47 | print('receiving data...') 48 | 49 | data = recvall(conn) 50 | 51 | with open(path, 'wb') as f: 52 | print('file opened') 53 | 54 | print('data=%s', data) 55 | f.write(data) 56 | print("Server: Done transfer") 57 | 58 | conn.close() 59 | 60 | 61 | if __name__ == '__main__': 62 | choices = {'client': client, 'server': server} 63 | parser = argparse.ArgumentParser(description="transfer file") 64 | parser.add_argument('role', choices=choices) 65 | parser.add_argument('host') 66 | parser.add_argument('-p', metavar='PORT', type=int, default=1060) 67 | args = parser.parse_args() 68 | function = choices[args.role] 69 | function(args.host, args.p) -------------------------------------------------------------------------------- /transfer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chevalx/PythonChat/c26fc837c2ecdd79094e8b8fa4bea4475d23752c/transfer/__init__.py -------------------------------------------------------------------------------- /transfer/recv.txt: -------------------------------------------------------------------------------- 1 | test for file transfer -------------------------------------------------------------------------------- /transfer/test.txt: -------------------------------------------------------------------------------- 1 | test for file transfer --------------------------------------------------------------------------------