目录python
自从互联网诞生以来,如今基本上全部的程序都是网络程序,不多有单机版的程序了。
计算机网络就是把各个计算机链接到一块儿,让网络中的计算机能够互相通讯。网络编程就是如何在程序中实现两台计算机的通讯。
举个例子,当你使用浏览器访问新浪网时,你的计算机就和新浪的某台服务器经过互联网链接起来了,而后,新浪的服务器把网页内容做为数据经过互联网传输到你的电脑上。
因为你的电脑上可能不止浏览器,还有QQ、微信、邮件客户端等,不一样的程序链接的别的计算机也会不一样,因此,更确切地说,网络通讯是两台计算机上的两个进程之间的通讯。好比,浏览器进程和新浪服务器上的某个Web服务进程在通讯,而QQ进程是和腾讯的某个服务器上的某个进程在通讯。
网络编程对全部开发语言都是同样的,Python也不例外。用Python进行网络编程,就是在Python程序自己这个进程内,链接别的服务器进程的通讯端口进行通讯。shell
计算机为了联网,就必须规定通讯协议,早期的计算机网络,都是由各厂商本身规定一套协议,IBM、Apple和Microsoft都有各自的网络协议,互不兼容,这就比如一群人有的说英语,有的说中文,有的说德语,说同一种语言的人能够交流,不一样的语言之间就不行了。
后来为了打破这个局面,出现了一套全球通用协议族,叫作互联网协议,互联网协议包含了上百种协议标准,可是最重要的两个协议是TCP和IP协议,因此,你们把互联网的协议简称TCP/IP协议。
通讯的时候,双方必须知道对方的标识,比如发邮件必须知道对方的邮件地址。互联网上每一个计算机的惟一标识就是IP地址,是由4个点分十进制数组成(例如:12.21.21.41)。
下面是TCP/IP协议分层:
TCP/UDP协议则是创建在IP协议之上的。TCP协议负责在两台计算机之间创建可靠链接,保证数据包按顺序到达。TCP协议会经过握手创建链接,而后,对每一个IP包编号,确保对方按顺序收到,若是包丢掉了,就自动重发
。相对于TCP(面向链接)来讲,UDP则是面向无链接的协议,使用UDP协议时,不须要创建链接,只须要知道对方的IP地址和端口号,就能够直接发数据包
。可是,能不能到达就不知道了。虽然用UDP传输数据不可靠,但它的优势是和TCP比,速度快,对于不要求可靠到达的数据,就可使用UDP协议。
许多经常使用的更高级的协议都是创建在TCP协议基础上的,好比用于浏览器的HTTP协议、发送邮件的SMTP协议等。
一个IP包除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。那么端口有什么做用呢?在两台计算机通讯时,只发IP地址是不够的,由于同一台计算机上跑着多个网络程序。一个IP包来了以后,究竟是交给浏览器仍是QQ,就须要端口号来区分。每一个网络程序都向操做系统申请惟一的端口号,这样,两个进程在两台计算机之间创建网络链接就须要各自的IP地址和各自的端口号。编程
Socket称为安全套接字,是网络编程的一个抽象概念。一般咱们用一个Socket表示'打开了一个网络连接',而打开一个Socket须要知道目标计算机的IP地址和端口号,再指定协议类型便可。
大多数链接都是可靠的TCP链接。建立TCP链接时,主动发起链接的叫客户端,被动响应链接的叫服务器。
socket库是一个底层的用于网络通讯的库,使用它咱们能够便捷的进行网络交互的开发,下面以socket库为例,想要使用须要先引入import socket
json
咱们先来了解一下,python的socket的通信流程:
windows
服务端:数组
一个TCP端口只能被绑定一次
关闭链接浏览器
客户端关闭链接时,服务端只须要关闭与之相连的socket便可,服务端不用关闭,由于还有其余客户端会链接缓存
客户端:安全
服务端想要提供服务,首先须要绑定IP地址,而后启动服务,监听端口等待客户端的链接,一旦有客户端链接访问,那么接下来就能够接受客户端发送的数据了。根据上图,以及创建服务端的流程,我门来捋一下服务端的逻辑到代码的步骤:服务器
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # socke.AF_INET 指的是使用 IPv4 # socket.SOCK_STREAM 指定使用面向流的TCP协议
socket.bind(('127.0.0.1',999)) # 小于1024的端口只有管理员才能够指定
socket.listen()
sock, client_addr = socket.accept() # 返回二元组,socket链接和客户端的IP及Port元祖
data = sock.recv(1024) # 接收1024个字节的数据,通常是2的倍数,bytes格式
sock.send('data'.encode()) # bytes格式
sock.close()
完成的代码:
import socket socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) socket.bind(('127.0.0.1',999)) socket.listen() sock, client_info = socket.accept() data = sock.recv(1024) sock.send(data) sock.close() # 关闭客户端socket链接 socket.close() # 关闭服务器
根据TCP创建链接的三次握手机制,咱们知道客户端想要链接服务端,首先建立TCP链接,使用某个端口号,链接服务端,而后发送/接受数据,而后关闭链接。根据上图,以及创建客户端的流程,咱们来捋一下客户端的逻辑到代码的步骤:
socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 默认就是socket.AF_INET,socket.SOCK_STREAM,因此TCP时,能够直接socket.socket()
socket.connect('127.0.0.1',999)
socket.send('data'.encode())
data = socket.recv(1024)
socket.close()
完整的代码:
import socket socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) socket.connect(('127.0.0.1', 999)) socket.send(b'data') data = socket.recv(1024) print(data) socket.close() # 关闭客户端socket链接
在初始化时,socket方法提供了不一样的参数,用于指定不一样的连接类型以及不一样的IP地址类型。
IP协议相关:
AF_INET
:IPV4AF_INET
: IPV6AF_UNIX
: Unix Domain Socket(windows没有)Socket类型:
SOCK_STREM
: 面向链接的套接字。TCP协议SOCK_DGRAM
: 无链接的数据报文套接字。UDP协议默认状况下 socket.socket()的参数为AF_INET,SOCK_STREM,因此若是须要的是IPv4的TCP链接,能够直接实例化便可
服务器端套接字:
函数 | 描述 |
---|---|
s.bind() |
绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。 |
s.listen() |
开始TCP监听。backlog指定在拒绝链接以前,操做系统能够挂起的最大链接数量。该值至少为1,大部分应用程序设为5就能够了。 |
s.accept() |
被动接受TCP客户端链接,(阻塞式)等待链接的到来 |
客户端套接字:
函数 | 描述 |
---|---|
s.connect() |
主动初始化TCP服务器链接,。通常address的格式为元组(hostname,port),若是链接出错,返回socket.error错误。 |
s.connect_ex() | connect()函数的扩展版本,出错时返回出错码,而不是抛出异常 |
公共用途的套接字函数:
函数 | 描述 |
---|---|
s.recv() |
接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其余信息,一般能够忽略。 |
s.send() |
发送TCP数据,将string中的数据发送到链接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。 |
s.sendall() | 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到链接的套接字,但在返回以前会尝试发送全部数据。成功返回None,失败则抛出异常。 |
s.recvfrom() |
接收UDP数据,与recv()相似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。 |
s.sendto() |
发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。 |
s.close() |
关闭套接字 |
s.getpeername() | 返回链接套接字的远程地址。返回值一般是元组(ipaddr,port)。 |
s.getsockname() | 返回套接字本身的地址。一般是一个元组(ipaddr,port) |
s.setsockopt(level,optname,value) | 设置给定套接字选项的值。 |
s.getsockopt(level,optname[.buflen]) | 返回套接字选项的值。 |
s.settimeout(timeout) | 设置套接字操做的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。通常,超时期应该在刚建立套接字时设置,由于它们可能用于链接的操做(如connect()) |
s.gettimeout() | 返回当前超时期的值,单位是秒,若是没有设置超时期,则返回None。 |
s.fileno() | 返回套接字的文件描述符。 |
s.setblocking(flag) |
若是flag为0,则将套接字设为非阻塞模式,不然将套接字设为阻塞模式(默认值)。非阻塞模式下,若是调用recv()没有发现任何数据,或send()调用没法当即发送数据,那么将引发socket.error异常。 |
s.makefile() |
建立一个与该套接字相关连的文件 |
这里单独把makefile方法抽出来,是由于它可让咱们用操做文件的方式来操做socket。makefile的用法以下:
makefile(self, mode="r", buffering=None, *,encoding=None, errors=None, newline=None):
看这些参数是否是很眼熟?没错,和open函数的参数差很少是相同的,默认状况下模式为r,若是是socket的话,咱们知道能够接受数据,也能够发送数据,对应的文件上的话,就是能够读取也能够写入,因此模式应该为'rw'
。
makefile的mode模式,只有'rw',没有'r+',这点和文件打开方式不一样。
import socket server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.1', 8080)) server.listen(5) while True: # 连接循环 print('wait for connect') conn, addr = server.accept() print('client connect', addr) f = conn.makefile(mode='rw') # 建立一个 类file对象 while True: try: # Windows下捕捉客户端异常关闭链接 print('~~~~~~~~~') client_msg = f.readline() # 从缓冲区中读取数据 print(client_msg) if not client_msg: break # Linux下处理客户端异常退出问题 print('client msg :', client_msg) f.write(client_msg.upper()) # 向缓冲区写入数据 f.flush() except (ConnectionResetError, Exception): # except能够同时指定多个异常 print('1') break conn.close() server.close()
直接用不太好用,使用read方法时,因为没法知道要读取多少字节,因此会有各类问题,能够引用封装,将要发送的数据总大小,按照固定4个字节发到服务端,告诉服务端后面的数据有多少,而后服务端动态指定read的字节数便可。
上面写的代码只能通信一次,就结束链接了。正经的socket交互是那种有来有往的,并非这样这种,因此咱们须要进行修改。
抛出问题:
针对问题作以下改进:
服务端:增长循环,完成通讯循环,而且把客户端发来的消息转换成大写的并返回。
import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) print('wait for connect') conn,addr = server.accept() print('client connect',addr) while True: #循环的接受消息 client_msg = conn.recv(1024) print('client msg :', client_msg) conn.send(client_msg.upper()) conn.close() server.close()
客户端:增长循环,完成通讯循环,而且发送的消息由用户来输入,当输入为空的时候,继续循环。
import socket client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: # 通讯循环 msg = input('>>:').strip() if not msg:continue # 当用户输入为空的时候,继续循环 client.send(msg.encode('utf-8')) server_msg = client.recv(1024) print(server_msg.decode('utf-8')) client.close()
抛出问题:
因为问题集中在服务端,因此对服务端作以下改进:
import socket server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8080)) server.listen(5) while True: #连接循环 print('wait for connect') conn,addr = server.accept() print('client connect',addr) while True: try: #Windows下捕捉客户端异常关闭链接 client_msg = conn.recv(1024) if not client_msg:break #Linux下处理客户端异常退出问题 print('client msg :', client_msg) conn.send(client_msg.upper()) except (ConnectionResetError,Exception): #except能够同时指定多个异常 break conn.close() server.close()
客户端异常关闭时,服务端的异常为:ConnectionResetError,咱们能够经过捕捉其,来控制服务端的推出,也可使用 Exception(通用)异常来捕捉。
利用socket,远程执行命令,并返回,模拟ssh的效果
以上需求都针对服务端,那么对服务端作以下修改
import socket from subprocess import Popen,PIPE server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # server.bind(('192.168.56.200',8080)) server.bind(('127.0.0.1',8080)) server.listen(5) while True: print('wait for connect') conn,addr = server.accept() print('client connect',addr) while True: try: cmd = conn.recv(1024).strip() if not cmd:break p = Popen(cmd.decode('utf-8'),shell=True,stdout=PIPE,stderr=PIPE) stdout,stderr = p.communicate() #执行的结果就是bytes格式的string if stderr: conn.send(stderr) else: conn.send(stdout) except (ConnectionResetError,Exception): break conn.close() server.close()
因为咱们在接受和发送数据的时候,都指定了每次接收1024个字节的数据,而发送的数据咱们是不可估量的,若是发送的时候超过1024字节,那么在接收端就没法一次收取完毕,这些数据会存放在操做系统缓存中,那么下次再接收1024字节的数据的时候,会从缓存中继续读取,那么就会发生粘包现象。
所谓粘包问题主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所形成的。
只有TCP有粘包现象,UDP永远不会粘包
不是server端直接发送,client端直接接收
整个过程如图:
发生黏包的本质问题是对端不知道咱们发送数据的总长度,若是可否让对方提早知道,那么就不会发生粘包现象
。根据TCP报文的格式获得启发:
若是咱们要预先传递数据的大小(int型),那么就须要把它看成数据传输,当服务端收到之后,就知道后续的数据大小了,那么一次传输的数据到底占多少字节呢,Python的struct模块能够帮助咱们实现这个过程,当传递诸如int、char之类的基本数据的时候,struct提供了一种机制将这些特定的结构体类型打包成二进制流的字符串而后再网络传输,接收端也应该能够经过struct模块进行解包还原出原始的结构体数据。
struct.pack('i',int) # i表示把数字用4个字节进行表示,这样的话就能够表示2的32次方的数字,已经知足需求 # 后面的int表示要打包的数字(要发送的报文长度) # 经过struct.pack 会获得bytes格式的数据,能够直接进行发送
struct.unpack('i',obj) # obj表示收取到数据 # 会返回一个元组,元组的第一个元素为对方传过来的报文长度 # 能够复制给一个变量来指定接收的报文长度
更多的用法需自行查找struct模块的官方文档。
# 服务端 #!/usr/bin/env python # Author:Lee Sir #_*_ coding:utf-8 _*_ import socket from subprocess import Popen,PIPE import struct server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.bind(('127.0.0.1',8000)) server.listen(5) while True: print('等待链接......') conn,addr = server.accept() print('客户端地址为:',addr) while True: try: cmd_bytes = conn.recv(1024) if not cmd_bytes:continue cmd_str = cmd_bytes.decode('utf-8') print('执行的命令是:',cmd_str) #执行命令 p = Popen(cmd_str,shell=True,stdout=PIPE,stderr=PIPE) stdout,stderr = p.communicate() #返回的数据 if stderr: send_data = stderr else: send_data = stdout #构建报头并发送报头 conn.send(struct.pack('i',len(send_data))) #发送数据 conn.send(send_data) except Exception: break
客户端
#!/usr/bin/env python # Author:Lee Sir #_*_ coding:utf-8 _*_ import socket import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8000)) while True: msg = input('Please input msg: ') if not msg:continue client.send(msg.encode('utf-8')) #接收报头,服务端使用i模式,因此固定是4个字节 server_data_head = client.recv(4) server_data_len = struct.unpack('i',server_data_head)[0] #根据传递的报头长度接收报文 server_data = client.recv(server_data_len) print(server_data.decode('gbk'))
当数据量比较大以及须要额外其余数据的场合下,以上的解决方案就有问题
针对上面的问题有如下解决方案:
服务端
#!/usr/bin/env python # Author:Lee Sir #_*_ coding:utf-8 _*_ import socket from subprocess import Popen,PIPE import struct import json server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) server.bind(('127.0.0.1',8080)) server.listen(5) while True: print('等待链接......') conn,addr = server.accept() print('客户端地址为:',addr) while True: try: cmd_bytes = conn.recv(1024) if not cmd_bytes:continue cmd_str = cmd_bytes.decode('utf-8') print('执行的命令是:',cmd_str) #执行命令 p = Popen(cmd_str,shell=True,stdout=PIPE,stderr=PIPE) stdout,stderr = p.communicate() #返回的数据 if stderr: send_data = stderr else: send_data = stdout #建立报头内容及获取包头长度 file_dict = {'filename':None,'hash':None,'size':len(send_data)} file_json = json.dumps(file_dict).encode('utf-8') file_json_len = len(file_json) #构建报头 file_head = struct.pack('i',file_json_len) #发送报头长度 conn.send(file_head) #发送报头 conn.send(file_json) #发送数据 conn.send(send_data) except Exception: break
客户端:
#!/usr/bin/env python # Author:Lee Sir import socket import struct import json client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg = input('Please input msg: ') if not msg:continue client.send(msg.encode('utf-8')) #接收报头,服务端使用i模式,因此固定是4个字节 server_file_head = client.recv(4) server_file_len = struct.unpack('i',server_file_head)[0] #接收报文头部信息 server_head_file = client.recv(server_file_len) #报文头部信息 server_head = json.loads(server_head_file.decode('gbk')) #获取报文的头部信息 server_file_name = server_head['filename'] server_file_hash = server_head['hash'] server_file_size = server_head['size'] #根据传递的报头长度分段接收报文 recv_len = 0 server_data = b'' while recv_len < server_file_size: recv_data = client.recv(1024) server_data += recv_data recv_len += len(recv_data) print(server_data.decode('gbk'))
下面咱们来写一个小项目,聊天室,客户端发送的消息须要转发给全部已在线的客户端,下面是实现方法:
服务端代码:
import socket import threading def recv(s: socket.socket, clients, lock): addr = s.getpeername() # 获取对端IP地址 # 通讯循环 while True: try: data = s.recv(1024) print(data) if not data: break # 群发消息,须要加锁,防止在遍历的同时,客户端断开链接时,触发字典修改操做 with lock: for conn in clients.values(): conn.send('{}:{} {}'.format(*addr,data.decode()).encode()) except (ConnectionResetError, OSError): # 当客户端断开链接时 s.close() # 在已链接列表中删除关闭的链接 with lock: clients.pop(addr) break def accept(server: socket.socket, clients, lock): # 链接循环等待客户端链接 while True: conn, addr = server.accept() print('{} is comming'.format(addr)) with lock: clients[addr] = conn threading.Thread(target=recv, name='recv', args=(conn, clients, lock)).start() # 启动链接线程 if __name__ == '__main__': # 存放全部client列表,用于消息群发 clients = {} # 建立锁文件,在修改clients时加锁 lock = threading.Lock() server = socket.socket() server.bind(('127.0.0.1', 9999)) server.listen() print('start Server!!!') # 启动accept线程 threading.Thread(target=accept, name='accept', args=(server, clients, lock), daemon=True).start() while True: cmd = input('>>>>').strip().lower() if cmd == 'quit': break else: print(threading.enumerate()) server.close()
客户端代码:
import socket import threading def recvdata(s: socket.socket, event: threading.Event): while True: try: data = s.recv(1024) print(data.decode()) except (ConnectionResetError, OSError): event.set() # 若是服务端断开链接,触发事件 break if __name__ == '__main__': event = threading.Event() client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 9999)) # 启动接受线程 threading.Thread(target=recvdata, name='recv', args=(client, event)).start() # 通信循环,当服务端断开链接时,结束 while not event.is_set(): msg = input('>>:').strip() if not msg: continue if msg.upper() == 'quit': break client.send(msg.encode('utf-8')) print('服务端断开') client.close()
服务端:
import socket import threading import datetime import logging FORMAT = '%(asctime)s %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) class ChatTcpServer: """ self.ip: 服务端地址 self.port:服务端端口 self.socket:建立一个socket对象,用于socket通讯 self.event:建立一个事件对象,用于控制连接循环 self.clients:记录当前已链接的客户端 self.lock:用于多线程添加修改clients对象时的锁 """ def __init__(self, ip, port): self.ip = ip self.port = port self.socket = socket.socket() self.event = threading.Event() self.clients = {} self.lock = threading.Lock() def start(self): self.socket.bind((self.ip, self.port)) self.socket.listen() threading.Thread(target=self.accept, name='accept', daemon=True).start() logging.info('ChatServer Starting!!!') def accept(self): while not self.event.is_set(): conn, client_addr = self.socket.accept() # 把链接的客户端保存,用于广播消息 with self.lock: self.clients[client_addr] = conn logging.info('{}:{} is comming'.format(*client_addr)) threading.Thread(target=self.recv, name='recv', args=(conn, client_addr), daemon=True).start() def recv(self, sock, client_addr): while True: try: data = sock.recv(1024) # windows 代码客户端主动关闭时,不会发送b'',服务端会直接异常。这里添加异常捕捉,当客户端强制关闭时,删除socket except (ConnectionResetError, OSError): with self.lock: self.clients.pop(client_addr) logging.info('{}:{} is down'.format(*client_addr, )) break # 某些客户端在强制关闭时会发送b'',这里添加相关判断 if data == b'quit' or data == b'': with self.lock: self.clients.pop(client_addr) logging.info('{}:{} is down'.format(*client_addr, )) break # 日志及消息信息 logging.info('{}:{} {}'.format(*client_addr, data.decode())) msg = '{} {}:{} {}'.format(datetime.datetime.now(), *client_addr, data.decode()).encode() # 广播发送消息 with self.lock: for client in self.clients.values(): client.send(msg) def stop(self): self.event.set() # 关闭全部还在存活的client链接 with self.lock: for client in self.clients.values(): client.close() self.socket.close() def main(): cts = ChatTcpServer('127.0.0.1', 9999) cts.start() while True: cmd = input('>>>').strip() if cmd.lower() == 'quit': cts.stop() break else: print(threading.enumerate()) if __name__ == '__main__': main()
客户端:
import socket import threading import datetime import logging FORMAT = '%(asctime)s %(message)s' logging.basicConfig(level=logging.INFO, format=FORMAT) class ChatTCPClient: """ self.ip: 服务端地址 self.port:服务端端口 self.socket:建立一个socket对象,用于socket通讯 self.event:建立一个事件对象,用于控制连接循环 """ def __init__(self, ip, port): self.ip = ip self.port = port self.socket = socket.socket() self.event = threading.Event() def connect(self): self.socket.connect((self.ip, self.port)) threading.Thread(target=self.recv, name='recv',daemon=True).start() def recv(self): while not self.event.is_set(): # 某些服务端强制关闭时,会出b'',这里进行判断 try: data = self.socket.recv(1024) if data == b'': self.event.set() logging.info('{}:{} is down'.format(self.ip, self.port)) break logging.info(data.decode()) # 有些服务端在关闭时不会触发b'',这里会直接提示异常,这里进行捕捉 except (ConnectionResetError,OSError): self.event.set() logging.info('{}:{} is down'.format(self.ip, self.port)) def send(self, msg): self.socket.send(msg.encode()) def stop(self): self.socket.close() if __name__ == '__main__': ctc = ChatTCPClient('127.0.0.1', 9999) ctc.connect() while True: info = input('>>>>:').strip() if not info: continue if info.lower() == 'quit': logging.info('bye bye') ctc.stop() break if not ctc.event.is_set(): ctc.send(info) else: logging.info('Server is down...') break