介绍:

  • 面向Socket编程进阶(具体客户端,服务端语法)
  • 使用线程使得服务端服务多个用户
  • 用户之间的通信

面向Socket编程进阶

服务端

创建Socket服务端的步骤

  • 创建服务端端套接字对象 创建服务端socket对象 ==>so = socket.socket(AddressFamily, Type)
  • 绑定端口号 bind((host, port)) 表示绑定端口号, host 是 ip 地址,port 是端口号
  • 设置监听 listen (backlog) 表示设置监听,backlog参数表示最大等待建立连接的个数(128)
  • 等待接受客户端的连接请求 accept() 表示等待接受客户端的连接请求
  • 接收数据 recv(buffersize) 表示接收数据, buffersize 是每次接收数据的长度
  • 发送数据 send(data) 表示发送数据,data 是二进制数据
  • 关闭套接字 so.close()

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket

if __name__ == "__main__":
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 这里面不传参也行(ipv4,tcp)
server.bind(('0.0.0.0',8000)) # 绑定端口(元组)
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
while True:
server.listen(128) # 128表示最大等待建立连接的个数
# accept()等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
service_socket, ip_port = server.accept() # service_socket是专门和客户端通信的套接字,ip_port是客户端ip端口
print("客户端的端口号:",ip_port)
recv_data = service_socket.recv(1024) # 接收客户端发来的消息,最大字节1024
recv_data_length = len(recv_data) # 查看长度
recv_content = recv_data.decode("gbk")
print("客户端发来的数据是:",recv_content)
info = "我知道了小威".encode("gbk")
service_socket.send(info) # 向客户端发送消息

客户端

创建客户端步骤:

  • 创建客户端套接字对象 so = socket.socket(AddressFamily, Type)
  • 和服务端套接字建立连接 so.connect((host, port))
  • 发送数据 so.send(data)
  • 接收数据 recv(buffersize) 表示接收数据, buffersize是每次接收数据的长度
  • 关闭客户端套接字 so.close()

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
import socket

if __name__ == '__main__':
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建tcp客户端套接字
client.connect(('10.211.55.32',8000)) # 向服务端建立连接
# 执行到此表明连接成功
print("连接成功!")
data = "你好,我是cilent小威".encode("gbk") # gbk是万国码
client.send(data) # 向服务端发送数据
accept_data = client.recv(1024) # 接收服务器发来的数据
recv_data = accept_data.decode("gbk") # 解码
print("服务器返回的数据是:",recv_data)

多任务版TCP服务端程序开发

具体实现步骤

  1. 编写一个TCP服务端程序,循环等待接受客户端的连接请求
  2. 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞
  3. 把创建的子线程设置成为守护主线程,防止主线程无法退出。
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
import threading
import socket

def handle_client_request(service_sock,ip_port):
# 处理客户端请求(service_sock,ip_port)作为参数
while True:
data = service_sock.recv(1024)
if data:
print(data.decode("gbk"),ip_port)
service_sock.send("好的已收到!".encode("gbk"))
else:
print("客户端下线了!",ip_port)
break


if __name__ == '__main__':
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #ipv4, tcp
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) # 设置端口号复用,程序退出立即释放端口号
server.bind(('0.0.0.0', 8000)) # 绑定端口和ip服务器地址
server.listen(128) # 设置最大等待连接个数为128
while True:
service_sock, ip_port = server.accept() # 循环等待接受客户端的连接请求
handle_thread = threading.Thread(target=handle_client_request,args=(service_sock,ip_port))
handle_thread.setDaemon(True) # 设置守护主线程
handle_thread.start() # 启动

用户间通信

如何实现用户之间的通信?用户之间能直接通信吗?

很久之前我就是以为用户之间能直接通信,但是学了面向Socket编程,才发现用户之间是不能直接通信的,要通过服务器的转发来间接实现用户之间的通信。

下面的代码是我很久之前写的代码,过了两年忘的一干二净。哈哈!由此,我愈来愈感受到写注释的重要,以及时时刻刻做好记录是多么的重要。

QQ_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
import socket
import json
import threading
from collections import defaultdict

online_users = defaultdict(dict) # 维护用户连接
# 创建了一个 defaultdict 对象,其中每个键对应的默认值是一个空列表。
# 也就是说,如果你通过这个 defaultdict 对象查询一个不存在的键,那么该键将自动被添加到字典中,并且其对应的值将是一个空列表。
# 这种数据结构通常用于处理字典中缺失键的情况,可以避免 KeyError 异常的发生。
user_msgs = defaultdict(list) # 维护用户的历史消息

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 设置ipv4,TCP连接
server.bind(('0.0.0.0', 8000)) # 绑定ip和端口
server.listen(128) # 设置最大等待连接数

def handle_client_request(sock, addr):
while True:
recv_data = sock.recv(1024) # 接收用户发送的数据,最大1024字节
json_data = json.loads(recv_data.decode("utf8")) # 将用户发送的字符串(json转换为字典类型
action = json_data.get("action", "") # 获取字典中action键的值==json_data.get("action")
# json_data 是用户发来的数据,action是用户的行为,action键的值
if action == 'login':
online_users[json_data["user"]] = sock # 将sock添加到字典中"user": sock
sock.send("登陆成功".encode("utf8"))
elif action == 'list_user':
# 获取当前用户列表
all_users = [user for user, sock in online_users.items()]
# 列表推导式:user-键,sock-值。最终结果列表里是online_users的所有键
sock.send(json.dumps(all_users).encode("utf8"))
# 向客户端发送all_users列表(列表已dumps成字符串
elif action == 'history_msg':
# 用户获取历史消息
sock.send(json.dumps(user_msgs.get(json_data["user"])).encode("utf8"))
# 向客户端发送(将用户的历史消息发送过去,如果该用户的历史消息不存在就默认为空列表
elif action == 'send_msg':
# 用户要发送信息
if json_data["to"] in online_users:
online_users[json_data["to"]].send(json.dumps(json_data).encode("utf8"))
user_msgs[json_data["to"]].append(json_data)
# 将用户发送的消息追加到历史消息末尾
elif action == 'exit':
del online_users[json_data["user"]]
# 删除当前在线用户字典'user'的值(字典值的删除方式
sock.send("退出成功".encode("utf8")) # 向用户发送消息


if __name__ == '__main__':
while True:
sock, addr = server.accept() # 循环阻塞等待连接
client_thread = threading.Thread(target=handle_client_request,args=(sock,addr)) # 创建子线程处理用户请求
client_thread.start() # 启动线程

QQ_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
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import socket
import json
import threading

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建客户端套接字ipv4,tcp
client.connect(('10.211.55.32', 8000)) # 和服务端建立连接

user = 'Penguin' # 设定用户名

# 登陆
login_templete = {
"action": "login",
"user": user
}

# 向服务端发送字符串
client.send(json.dumps(login_templete).encode('utf8')) # 将字典文件dumps为jsonl类型(不过通常是str类型
res = client.recv(1024) # 接收服务器发来的信息,最大字节为1024
print(res.decode('utf8'))

# 获取当前在线用户
get_user_templete = {
"action": "list_user"
}

client.send(json.dumps(get_user_templete).encode('utf8')) # 向服务端发送获取当前在线用户的请求
res = client.recv(1024)
print("当前在线用户:{}".format(res.decode("utf8"))) # fotmat作为Python的的格式字符串函数,主要通过字符串中的花括号{},来识别替换字段,从而完成字符串的格式化

# 获取历史消息
offonline_msg_templete = {
"action": "history_msg",
"user": user
}
client.send(json.dumps(offonline_msg_templete).encode("utf8")) # 用户发送的都是字符串
res = client.recv(1024)
print("历史消息{}".format(res.decode("utf8")))

exit = False # 设置信号量决定是否执行处理逻辑

def handle_receive():
# 处理接收请求
while True:
if not exit:
# 如果 exit 为False,则代码块执行。如果exit为True,则代码块不会被执行。
# 需要注意的是,在Python中,空容器、0、None等值会被视为False,非空容器、非零数、非None值会被视为True。
try:
res = client.recv(1024)
except:
break # 跳出while循环
try: # 如果上一个try成立
res_json = json.loads(res) # 将json类型(字符串)的数据转换为字典
msg = res_json["data"]
from_user = res_json["from"]
print("收到来自用户({})的信息{}:".format(from_user,msg))
except:
print(res.decode("utf8"))
else: # exit信号标志位为True就跳出while循环
break


def handle_send():
# 随时发送消息
# 有消息能随时收到
while True:
op_type = input("请输入您要进行的操作:1,发送消息,2,退出,3,获取当前在线用户")
if op_type not in ["1", "2", "3"]:
print("不支持该操作")
op_type = input("请输入您要进行的操作:1,发送消息,2,退出,3,获取当前在线用户")
elif op_type == '1':
to_user = input("请输入您要发送到的用户:")
msg = input("请输入您要发送的信息:")
send_data_templete = {
"action": "send_msg",
"to": to_user,
"from": user,
"data": msg
}
client.send(json.dumps(send_data_templete).encode("utf8"))
# 告诉服务器要发送消息,给to_user发送,来自user,以及发送的消息,并dumps成字符串然后转二进制
elif op_type == '2':
exit_templete = {
"action": "exit",
"user": user
}
client.send(json.dumps(exit_templete).encode("utf8"))
# 告诉服务器action="exit"
exit = True # 将exit标志位设置为True
client.close() # 关闭客户端连接
elif op_type == '3':
get_user_templete = {
"action": "list_user"
}
client.send(json.dumps(get_user_templete).encode("utf8"))
# 告诉服务器action=list_user,获取在线用户


if __name__ == '__main__':
send_thread = threading.Thread(target=handle_send) # 创建线程处理发送请求
send_thread.start() # 开启线程
recv_thread = threading.Thread(target=handle_receive) # 创建线程处理接收请求
recv_thread.start()

搭建静态Web服务器

访问固定页面

实现步骤:

  1. 编写一个TCP服务端程序
  2. 获取浏览器发送的http请求报文数据
  3. 读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。
  4. HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。

浏览器在地址页面输入:http://localhost:8000/即可访问web页面

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
import socket

if __name__ == '__main__':
# 服务端返回给客户端具体页面
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 创建服务端套接字
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,True)
# 设置端口复用,程序退出立即释放端口
server.bind(('0.0.0.0', 8000))
# 绑定端口
server.listen(128)
# 设置监听最大等待建立连接个数128
while True:
sock, addr = server.accept()
# 等待接收客户端的连接请求
recv_data = sock.recv(4096)
# 设置最大接收字节数
data = recv_data.decode("utf8")
print("客户端发送的数据是:{}".format(data))

with open('2/page01.html','rb') as f:
f_data = f.read()

# 响应行
response_line = "HTTP/1.1 200 ok \r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = f_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf8") + response_body

# 发送数据
sock.send(response_data)

sock.close()

访问指定页面

返回指定页面数据的实现步骤:

  1. 获取用户请求资源的路径
  2. 根据请求资源的路径,读取指定文件的数据
  3. 组装指定文件数据的响应报文,发送给浏览器
  4. 判断请求的文件在服务端不存在,组装404状态的响应报文,发送给浏览器

注意:

  • 被返回的HTML文件被当做请求体的时候不需要解码,即encode(“utf8”)
  • 响应行”HTTP/1.1 200 ok\r\n”和响应头”Server: PWS1.0\r\n”要写对
  • try,except,else,finally中else 是没有异常才执行,finally有没有异常都要执行
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
71
72
73
import socket

def main():
# 创建服务端套接字
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口复用,程序退出立即释放端口
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口
server.bind(('0.0.0.0', 8000))
# 设置监听最大等待建立连接个数128
server.listen(128)
while True:
# 等待接收客户端的连接请求
sock, addr = server.accept()
# 代码执行到此,说明连接建立成功
recv_data = sock.recv(4096)
if len(recv_data) == 0:
print("关闭浏览器了")
sock.close()
return

# 对二进制进行解码
recv_data_content = recv_data.decode("utf8")
print(recv_data_content)
# 对接收到的数据进行分割,最大分割次数指定为2
request_list = recv_data_content.split(" ", maxsplit=2)

# 获取请求资源路径
request_path = request_list[1]
print("客户端请求的页面是:",request_path)

# 判断请求的是否为根目录,如果是,返回首页数据
if request_path == "/":
request_path = "/index.html"

try:
# 动态打开指定文件
with open("static" + request_path, 'rb') as f:
# rb以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式
file_data = f.read()
except Exception as e:
# 如果存在异常
# 请求的资源不存在,返回404页面
# 响应行
response_line = "HTTP/1.1 404 Not Found\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
with open("static/error.html", "rb") as f:
file_data = f.read()
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf8") + response_body
# 发送信息
sock.send(response_data)
else: # else 是没有异常
# 响应行
response_line = "HTTP/1.1 200 ok\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf8") + response_body
# 发送数据
sock.send(response_data)
finally:
# finally是有没有异常都要执行
sock.close()


if __name__ == '__main__':
main()

多线程服务器

目前的Web服务器,不能支持多用户同时访问,只能一个一个的处理客户端的请求,那么如何开发多任务版的web服务器同时处理多个客户端的请求?

可以使用多线程,比进程更加节省内存资源。

多任务版web服务器程序的实现步骤:

  1. 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的请求,防止主线程阻塞。
  2. 把创建的子线程设置成为守护主线程,防止主线程无法退出。
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
71
72
73
74
75
76
77
78
79
80
81
82
83
import socket
import threading


def handle_client_request(sock, addr):
# 根据用户的请求返回指定页面的内容
recv_data = sock.recv(4096)
# 判断用户是发送了请求
if len(recv_data) == 0:
print("用户关闭了浏览器!")
# 关闭连接
sock.close()
# 跳出函数handle_client_request
return

# 将用户发送的信息解码
recv_data_content = recv_data.decode("utf8")
# 对接收到的数据进行分割,最大分割次数为2
request_list = recv_data_content.split(" ", maxsplit=2)

# 获取请求资源路径
request_path = request_list[1]
print("客户端请求的页面是:", request_path)

# 若客户端请求的是根目录下的文件
if request_path == "/":
request_path = "static/index.html"
try:
# 动态打开指定文件
with open(request_path, "rb") as f:
file_data = f.read()
except Exception as e:
# 如果用户请求别的路径
with open("static/error.html", "rb") as f:
file_data = f.read()
# 响应行
response_line = "HTTP/1.1 404 Not Found\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header).encode("utf8") + response_body
# 发送消息
sock.send(response_data)
else:
# 如果没有异常
# 响应行
response_line = "HTTP/1.1 200 ok\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf8") + response_body
sock.send(response_data)
finally:
# 有没有异常都要执行这句
sock.close()


def main():
# 创建服务端套接字,参数表示ipv4,和TCP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定服务器ip和端口
server.bind(('0.0.0.0', 8000))
# 监听,表示最大等待连接个数为128个
server.listen(128)
# 循环等待接收客户端的连接请求
while True:
sock, addr = server.accept()
# 创建线程处理用户的请求(args是以元组形式传递参数,kwargs以字典形式传参
handle_res = threading.Thread(target=handle_client_request, args=(sock, addr))
# 设置守护主线程
handle_res.setDaemon(True)
# 线程开始
handle_res.start()


if __name__ == '__main__':
main()