网络编程

Socket介绍

Socket套接字
Python中提供socket.py标准库,非常底层的接口库。
Socket是一种通用的网络编程接口,和网络层次没有一一对应的关系。

协议族
AF表示Address Family,用于socket()第一个参数

名称 含义
AF_INET IPV4
AF_INET6 IPv6
AF_UNIX Unix Domain Socket,windows没有

socket类型
| 名称 | 含义 |
| ——– | ——————————- |
| SOCK_STREAM| 面向连接的流套接字。默认值,TCP协议|
|SOCK_DGRAM |无连接的数据报文套接字。UDP协议 |

TCP编程

Socket编程,需要两端,一般来说需要一个服务端、一个客户端,服务端称为Server,客户端称为Client

TCP服务端编程


服务器端编程步骤

  • 创建Socket对象

  • 绑定IP地址Address和端口Port。bind()方法

    IPv4地址为一个二元组(‘IP地址字符串’, Port)

  • 开始监听,将在指定的IP的端口上监听。listen()方法

  • 获取用于传送数据的Socket对象 socket.accept() -> (socket object, address info)
    accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组
    地址是远程客户端的地址,IPv4中它是一个二元组(clientaddr, port)

    • 接收数据
      recv(bufsize[, flags]) 使用缓冲区接收数据

    • 发送数据
      send(bytes)发送数据

查看监听端口

1
2
3
4
5
6
windows 命令
# netstat -anp tcp | findstr 9999

linux命令
# netstat -tanl | grep 9999
# ss -tanl | grep 9999

练习——写一个一对多通信

需求分析

聊天工具是CS程序,C是每一个客户端,S是服务器端。

服务器应该具有的功能:
启动服务,包括绑定地址和端口,并监听
建立连接,能和多个客户端建立连接
接收不同用户的信息
分发,将接收的某个用户的信息转发到已连接的所有客户端
停止服务
记录连接的客户端

代码实现

服务端应该对应一个类

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
import logging
import socket
import threading
import datetime

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")


class ChatServer:
def __init__(self, ip='127.0.0.1', port=9999): # 启动服务
self.sock = socket.socket()
self.addr = (ip, port)
self.clients = {} # 客户端
self.event = threading.Event()

def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
#accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()

def accept(self): # 多人连接
while not self.event.is_set():
sock, client = self.sock.accept() # 阻塞
self.clients[client] = sock # 添加到客户端字典
#准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, args=(sock, client)).start()

def recv(self, sock:socket.socket, client): # 接收客户端数据
while not self.event.is_set():
data = sock.recv(1024) # 阻塞到数据到来
msg = data.decode().strip()
# 客户端退出命令
if msg == 'quit' or msg == '':
self.clients.pop(client)
sock.close()
logging.info('{} quits'.format(client))
break

msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *client,data.decode())
logging.info(msg)
msg = msg.encode()
for s in self.clients.values():
s.send(msg)

def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.close()
self.event.set()

cs = ChatServer()
cs.start()

while True:
cmd = input('>>').strip()
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate()) # 用来观察断开后线程的变化

socket常用的方法


名称 含义
socket.recv(bufsize[, flags]) 获取数据。默认是阻塞的方式
socket.recvfrom(bufsize[, flags]) 获取数据,返回一个二元组(bytes, address)
socket.recv_into(buffer[, nbytes[,flags]]) 获取到nbytes的数据后,存储到buffer中。如果nbytes没有指定或0,将buffer大小的数据存入buffer中。返回接收的字节数。
socket.recvfrom_into(buffer[,nbytes[, flags]]) 获取数据,返回一个二元组(bytes, address)到buffer中
socket.send(bytes[, flags]) TCP发送数据
socket.sendall(bytes[, flags]) TCP发送全部数据,成功返回None
socket.sendto(string[,flag],address) UDP发送数据
socket.sendfile(file, offset=0,count=None) 发送一个文件直到EOF,使用高性能的os.sendfile机制,返回发送的字节数。如果win下不支持sendfile,或者不是普通文件,使用send()发送文件。offset告诉起始位置。3.5版本开始
socket.getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)
socket.getsockname() 返回套接字自己的地址。通常是一个元组(ipaddr,port)
socket.setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
socket.settimeout(value) 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
socket.setsockopt(level,optname,value) 设置套接字选项的值。比如缓冲区大小。太多了,去看文档。不同系统,不同版本都不尽相同

MakeFile


socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None, newline=None)
创建一个与该套接字相关连的文件对象,将recv方法看做读方法,将send方法看做写方法。

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
# 使用makefile简单例子
import socket
sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip, port)
sockserver.bind(addr)
sockserver.listen()
print('-'30)
s, _ = sockserver.accept()
print('-'30)
f = s.makefile(mode='rw')

line = f.read(10) # 阻塞等
print('-'30)
print(line)
f.write('Return your msg: {}'.format(line))
f.flush()


# 改写成循环接收消息
import socket
import threading


sockserver = socket.socket()
ip = '127.0.0.1'
port = 9999
addr = (ip, port)
sockserver.bind(addr)
sockserver.listen()
print('-'30)

event = threading.Event()

def accept(sock:socket.socket, e:threading.Event):
s, _ = sock.accept()
f = s.makefile(mode='rw')

while True:
line = f.readline()
print(line)
if line.strip() == "quit": # 注意要发quit\n
break
f.write('Return your msg: {}'.format(line))
f.flush()
f.close()
sock.close()
e.wait(3)

t = threading.Thread(target=accept, args=(sockserver, event))
t.start()
t.join()

print(sockserver)

makefile练习


使用makefile改写一对多通信

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
import logging
import socket
import threading
import datetime


logging.basicConfig(level=logging.INFO, format="%(asctime)s %(thread)d %(message)s")

class ChatServer:
def init(self, ip='127.0.0.1', port=9999): # 启动服务
self.sock = socket.socket()
self.addr = (ip, port)
self.clients = {} # 客户端
self.event = threading.Event()

def start(self): # 启动监听
self.sock.bind(self.addr) # 绑定
self.sock.listen() # 监听
#accept会阻塞主线程,所以开一个新线程
threading.Thread(target=self.accept).start()

def accept(self): # 多人连接
while not self.event.is_set():
sock, client = self.sock.accept() # 阻塞
#准备接收数据,recv是阻塞的,开启新的线程
f = sock.makefile('rw') # 支持读写
self.clients[client] = f # 添加到客户端字典
threading.Thread(target=self.recv, args=(f, client), name='recv').start()

def recv(self, f, client): # 接收客户端数据
while not self.event.is_set():
try:
data = f.readline() # 阻塞到换行符
except Exception as e:
logging.error(e) # 有任何异常,退出
data = 'quit'
msg = data.strip()
#客户端退出命令
if msg == 'quit':
self.clients.pop(client)
f.close()
logging.info('{} quits'.format(client))
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *client,data)
logging.info(msg)

for s in self.clients.values():
s.write(msg)
s.flush()

def stop(self): # 停止服务
for s in self.clients.values():
s.close()
self.sock.close()
self.event.set()

def main():
cs = ChatServer()
cs.start()

while True:
cmd = input('>>').strip()
if cmd == 'quit':
cs.stop()
threading.Event().wait(3)
break
logging.info(threading.enumerate()) # 用来观察断开后线程的变化
if __name__ == '__main__':
main()

TCP客户端编程


客户端编程步骤

  • 创建Socket对象
  • 连接到远端服务端的IP和PORT、connect()方法
  • 传输数据
    • 使用send、recv方法发送、接收数据
  • 关闭连接、释放资源
1
2
3
4
5
6
7
8
import socket
client = socket.socket()
ipaddr = ('127.0.0.1', 9999)
client.connect(ipaddr) # 直接连接服务器
client.send(b'abcd\n')
data = client.recv(1024) # 阻塞等待
print(data)
client.close()

编写聊天的客户类

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
import socket
import threading
import datetime
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


class ChatClient:
def init(self, ip='127.0.0.1', port=9999):
self.sock = socket.socket()
self.addr = (ip, port)
self.event = threading.Event()

def start(self): # 启动对远端服务器的连接
self.sock.connect(self.addr)
self.send("I'm ready.")
#准备接收数据,recv是阻塞的,开启新的线程
threading.Thread(target=self.recv, name="recv").start()

def recv(self): # 接收客户端的数据
while not self.event.is_set():
try:
data = self.sock.recv(1024) # 阻塞
except Exception as e:
logging.error(e)
break
msg = "{:%Y/%m/%d %H:%M:%S} {}:{}\n{}\n".format(datetime.datetime.now(), *self.addr,data.strip())
logging.info(msg)

def send(self, msg:str):
data = "{}\n".format(msg.strip()).encode() # 服务端需要一个换行符
self.sock.send(data)

def stop(self):
self.sock.close()
self.event.wait(3)
self.event.set()
logging.info('Client stops.')

def main():
cc = ChatClient()
cc.start()
while True:
cmd = input('>>>')
if cmd.strip() == 'quit':
cc.stop()
break
cc.send(cmd) # 发送消息


if name == 'main':
main()