开发的第二阶段 网络编程阶段
之所以叫网络编程,是因为,这里面就不是你在一台机器中玩了.多台机器,CS架构.即客户端和服务器端通过网络进行通信的编程了.
首先想实现网络的通信,你得先学网络通信的一个基础,即两台机器之间是怎么打通的.有的同学说机器有IP地址,能互相ping通就连上了.
7层网络协议,第三层:网络层,第四层,传输层(tcp/ip协议).tcp/ip协议保证了两台机器的通信的可靠的数据传输(tcp/ip协议通信的3次握手).什么叫可靠的. A给B发消息,A发了,A会知道发没发到.
UDP不是可靠的数据传输协议,当A给B发消息,A发送出去了,A不知道B收没收到.A也不管.
UDP依然在使用.因为它快.
tcp/ip传输协议在发送数据前进行建立链接的3次握手.
3次握手后,才进行真正发送数据.那么问题来了.是什么东西在负责发送数据,对方又是什么东西在进行数据接收呢.那就是socket.
socket可以简单直白的认为它是一个管道,具体你在管道里传输什么socket不关心,如:mysql,http,这些对于socket来说就是一辆车.
Socket 基础
socket 通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.
socket起源于Unix,而Unix/Linux 基本哲学之一就是"一切皆文件",对于文件用[打开][读写][关闭]模式来操作.
socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO\打开\关闭)
socket和file的区别:
file模块是针对某个指定文件进行[打开][读写][关闭]
socket 模块是针对 服务器端 和 客户端socket 进行 [打开][读写][关闭]
socket基础使用实例:
socket_server.py
import socket
ip_port = ('127.0.0.1',9999) sk = socket.socket() #创建一个socket的句柄,如同open()一个文件 sk.bind(ip_port) #使用bind()方法,向系统内核注册一个ip,端口 作为这个socket的ip,port的属性,如果此端口没被占用,则
返回含有ip和端口的句柄,如果被占用则报错退出程序 sk.listen(5) #这个socket句柄处于监听状态。 while True: #写一个死循环,用来接收客户端对socket的链接请求 print('server waiting ......') conn,addr = sk.accept()
#程序执行到accept()时,阻塞在这里,当客户端使用connect()方法,发送连接请求,accept()方法把客户端的ip也就是addr作为参数,传入sk这个socket实例设置了客户端的ip端口属性,然后把返回实例的内存地址,赋值给新的变量conn。语法就是conn,addr = 实例名.accept()。 client_data = conn.recv(1024) #客户端和服务端的连接成功后,socket生成,紧接着在这里调用recv()方法,程序继续阻塞,直到客户端调用send方法,conn.recv(1024) 中的1024指的是一次最大接收1024个字节。 print(str(client_data,'utf8')) conn.send(bytes("不要回答,不要回答",'utf8')) #服务端发送即对socket套接字进行写操作。 conn.close()
socket_clinet.py
import socket
ip_port = ('127.0.0.1',9999)
sk = socket.socket()
sk.connect(ip_port)
sk.sendall(bytes("请求占领地球",'utf8'))
server_reply = sk.recv(1024)
print(str(server_reply,'utf8'))
sk.close()
基本的socket用法,很简单就实现了。那么我们深入一些:
1.能不能让客户端和服务端进行通信呢?
2.这是一个客户端,连接这个服务端。当多个客户端连接这个服务端会怎样?(这个测试下来,这种基本的socket是不能多个客户端访问的。)
那么我们来尝试解决问题1.
socket_server02.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' socket学习 ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print('server waiting ......') conn,addr = sk.accept() client_data = conn.recv(1024) print(str(client_data,'utf8')) conn.send(bytes("不要回答,不要回答",'utf8')) while True: client_data = conn.recv(1024) print(str(client_data,'utf8')) server_response = input(">>>") conn.send(bytes(server_response,'utf8')) conn.close()
socket_clinet02.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) sk.sendall(bytes("请求占领地球",'utf8')) server_reply = sk.recv(1024) print(str(server_reply,'utf8')) while True: user_input = input(">>>:").strip() sk.send(bytes(user_input,'utf8')) server_reply = sk.recv(1024) print(str(server_reply,'utf8')) sk.close()
先执行socket_server02.py
在执行socket_client02.py,执行结果如下:
socket_client02.py 不要回答,不要回答 #接到服务器的第一个响应 >>>:dd #客户端输入dd 你大爷 #哟,服务端骂人 >>>:龟孙 #骂回去 日你先人 #服务器又骂回来一句 >>>: socket_server02.py server waiting ...... 请求占领地球 #接到的第一个clinet send dd #服务端收到dd >>>你大爷 #服务端回复,d d看不懂,就说了句“你大爷” 龟孙 #乖乖,敢回嘴,我可是服务器 >>>日你先人 #接着骂
那么我这上面都是正常通信,输入的字符串都是正常的,假如客户端不输入了,直接退出。会怎样?
这里就不赘述老师的试验过程了,最终结果,老师说当clinet客户端停掉,windows和Linux的服务端对这个异常处理的方式不一样
但我自己在mac和Linux下尝试的结果一样,判断接收到clinet_data数据是否为空,为空就跳出循环。
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' socket学习 ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print('server waiting ......') conn,addr = sk.accept() client_data = conn.recv(1024) print(str(client_data,'utf8')) conn.send(bytes("不要回答,不要回答",'utf8')) while True: client_data = conn.recv(1024) if not client_data:break #判断当,对方传过来的值为空时,直接退出 print('client_data',str(client_data,'utf8')) server_response = input(">>>") conn.send(bytes(server_response,'utf8')) conn.close()
windows下,当客户端停掉,server端 conn.recv(1024)得到的数据不是空值,而是异常,异常发生后,程序就会退出,所以这里就要用到异常处理方法try语法,具体代码如下:这段代码如果用到Linux中,将无限循环下去,因为客户端停掉后,服务端会认为conn.recv(1024)得到空值,这样无限循环下去。
#!/usr/bin/env python3.5
#__author__:'ted.zhou'
'''
socket学习
'''
import socket
ip_port = ('127.0.0.1',9999)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)
while True:
print('server waiting ......')
conn,addr = sk.accept()
client_data = conn.recv(1024)
print(str(client_data,'utf8'))
conn.send(bytes("不要回答,不要回答",'utf8'))
while True:
try:
client_data = conn.recv(1024)
print('client_data',str(client_data,'utf8'))
except Exception:
print("clinet closed,break")
break
server_response = input(">>>")
conn.send(bytes(server_response,'utf8'))
conn.close()
使用socket写一个远程的ssh工具:
04socket_server.py代码如下:
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' socket学习,做成可以远程输入命令,客户端获得输出结果,如果你的命令输出结果很多,那么客户端得到的结 ''' import socket import subprocess ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print('server waiting ......') conn,addr = sk.accept() # client_data = conn.recv(1024) # print(str(client_data,'utf8')) # conn.send(bytes("不要回答,不要回答",'utf8')) while True: print("客户端{}已经连接,等待客户端传输命令... ...".format(conn.getpeername())) client_cmd_re = conn.recv(1024) if not client_cmd_re:break # 判断当,对方传过来的值为空时,直接退出(在客户端我已经判断用户输入不能为空,所以这里为空,只有一种可能,就是客户端关闭程序了.如果没有这句的break,会导致客户端一旦关闭,服务端也跟着关闭. # 因为当客户端关闭后,传过来的不知道是什么东西) print("用户传来了命令:%s"%client_cmd_re) # 打印输出用户传来的命令 cmd_re_str = str(client_cmd_re,'utf8') # 将用户传来的命令转换成str # 将命令执行,并获得执行结果 cmd_exec_result = subprocess.Popen(cmd_re_str,shell=True,stdout=subprocess.PIPE).stdout.read() # print(cmd_exec_result) # 打印执行的结果 # print(len(cmd_exec_result)) # 打印结果的长度 if len(cmd_exec_result) == 0 : # 判断结果是不是为空,为空说明用户传过来的命令,本服务器无法执行. cmd_exec_result = bytes("命令 '{}' 后没有返回执行结果,请检查命令是否正确...".format(cmd_re_str),'utf8') # 提示命令不对 #将执行结果发送给客户端 conn.send(cmd_exec_result) conn.close()
04socket_client.py代码如下:
#!/usr/bin/env python3.5 #__author__:'ted.zhou' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) # sk.sendall(bytes("请求占领地球",'utf8')) # # server_reply = sk.recv(1024) # print(str(server_reply,'utf8')) while True: input_cmd = input("cmd:").strip() # 用户输入命令 if input_cmd == 'q':break # 用户如果输入的是q,退出输入命令的循环 if not input_cmd:continue # 用户如果直接按了回车,既为空,则进行下次循环 print(input_cmd) # 打印此次用户输入的命令 sk.send(bytes(input_cmd,'utf8')) # 将用户的命令通过socket发送给服务器端 cmd_exec_data = sk.recv(1024) # 接收服务器执行命令后返回的结果 print(str(cmd_exec_data,'utf8')) sk.close()
上面的代码已经实现了,client连接到server后,进行基本的简单的命令。却不能进行实时的交互,比如top,cd 这种执行后没法返回的命令。具体如何时间,第七天的课程暂不说明。
那么问题来了,client端在接收设置中用到了sk.recv(1024),设置了每次最大收1024个字节,当你执行一个ifconfig命令,字节将大于1024,客户端就会一次收不完,这些数据,会怎样。这些数据已经被服务端通过socket发送过来了。你再次调用sk.recv(1024)时,继续接收余下的数据。那么怎样才能继续接收呢?目前的代码是循环到下次输入命令时会接收ifconfig命令结果的后续。那它这次的命令就又要排队到后面了。
那么如何进行循环接收呢?当然是在本次执行命令中,在sk.recv(1024)代码前加while 循环。
问题又来了。默认是死循环,while True,那么本次命令循环接收sk.recv(1024)结束条件怎么设置呢。
先说明:这里处理方法只有一种,就是在服务端传给客户端字符串前,先把传输的字节数告诉客户端。客户端循环接收字节,并把接收到的字节数累加统计,然后拿接收到的字节总数和服务端发来的做比较。如果小于继续循环。
但是: 老师给我们理清了思路,尝试了3种测试的方法,通过这3次尝试,让我们渐渐的了解socket传输的内部原理。
尝试方案1:
客户端在接收调用sk.recv(1024)方法,我们可以尝试调大这个数值,看看结果。是可以,每次多收一些,但是,如果你一个字符串有100*1024bytes,你不能把接收数值调整到100M吧,那内存还不爆掉。并且socket官方建议最大不能超过8192,所以此方案不可行。并且一般设置500即可。
尝试方案2:
我们在接收哪里直接while True,一直接收,直到接收不到数据时就退出break
06socket_server.py 不需要改动
06socket_client.py 代码如下:
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' 当服务端发送过来的字符串字节数过大,一次接收不完,客户端通过判断服务器端是不是发送完,来作为循环接收的依据。 ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) # sk.sendall(bytes("请求占领地球",'utf8')) # # server_reply = sk.recv(1024) # print(str(server_reply,'utf8')) while True: input_cmd = input("cmd:").strip() # 用户输入命令 if input_cmd == 'q':break # 用户如果输入的是q,退出输入命令的循环 if not input_cmd:continue # 用户如果直接按了回车,既为空,则进行下次循环 print(input_cmd) # 打印此次用户输入的命令 sk.send(bytes(input_cmd,'utf8')) # 将用户的命令通过socket发送给服务器端 res = bytes('','utf8') while True: cmd_exec_data = sk.recv(500) # 接收服务器执行命令后返回的结果 res += cmd_exec_data if not cmd_exec_data : #这里判断,当接收不到数据时,就退出循环 break print(str(res,'utf8')) sk.close()
ps:此方式不可行。原因是cmd_exec_data = sk.recv(500) 这里在接收完本次命令执行的所有结果后,会一直阻塞在这里。不会退出循环,此时客户端就卡死了。
总结:sk.recv()和sk.send()都会阻塞
尝试方案3:
在客户端判断服务端的数据是不是传完了,如果没传完,则继续循环接收。
05scoket_server.py 不需要改动
05socket_client.py 代码如下:
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' 当服务端发送过来的字符串字节数过大,一次接收不完,客户端通过判断服务器端是不是发送完,来作为循环接收的依据. 如何判断服务器端是不是发送完成? 我们认为,客户端每次接收的最大值500字节.那么如果最后一次传过来的数据长度没有500字节,那么我们是不是就可以说这次就是最后一次传输来. 所以while 循环中判断接收到的数据长度小于500,就退出循环接收. 代码如下: ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) # sk.sendall(bytes("请求占领地球",'utf8')) # # server_reply = sk.recv(1024) # print(str(server_reply,'utf8')) while True: input_cmd = input("cmd:").strip() # 用户输入命令 if input_cmd == 'q':break # 用户如果输入的是q,退出输入命令的循环 if not input_cmd:continue # 用户如果直接按了回车,既为空,则进行下次循环 print(input_cmd) # 打印此次用户输入的命令 sk.send(bytes(input_cmd,'utf8')) # 将用户的命令通过socket发送给服务器端 res = bytes('','utf8') while True: cmd_exec_data = sk.recv(500) # 接收服务器执行命令后返回的结果 res += cmd_exec_data if len(cmd_exec_data) < 500: #当接收到的字节小于500就判断已经接收完了。 break print(str(res,'utf8')) sk.close()
这里我们执行命令,得到一些数据量还没有那么大的字符串时没有问题,担当字符串相当大时,就出现之前的那种错误了,原因是我们最初认为的客户端每次都是按最大500字节接收(这个观点错了)。两个因素导致单次接收可能没有500字节。
因素1,服务端发送会因为网络状态发送数据。如果网络不稳定,传个10几个bytes也是可能的。
因素2,客户端接收也可能因为网络或者性能,导致这次接收延迟了几百毫秒(内部缓冲机制时间限制也许只有100毫秒)。从而导致本次接收不到500字节。
总结:客户端不是每次接收都是按照最大限额的字节接收的。这样我们的代码:
while True: cmd_exec_data = sk.recv(500) # 接收服务器执行命令后返回的结果 res += cmd_exec_data if len(cmd_exec_data) < 500: #这里就会在没接收完时,就退出了本次命令执行结果接收呢。 break
最终答案来了,代码如下:
07socket_server.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' socket学习,做成可以远程输入命令,客户端获得输出结果,如果你的命令输出结果很多,那么客户端得到的结 ''' import socket import subprocess ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print('server waiting ......') conn,addr = sk.accept() # client_data = conn.recv(1024) # print(str(client_data,'utf8')) # conn.send(bytes("不要回答,不要回答",'utf8')) while True: print("客户端{}已经连接,等待客户端传输命令... ...".format(conn.getpeername())) client_cmd_re = conn.recv(1024) if not client_cmd_re:break # 判断当,对方传过来的值为空时,直接退出(在客户端我已经判断用户输入不能为空,所以这里为空,只有一种可能,就是客户端关闭程序了.如果没有这句的break,会导致客户端一旦关闭,服务端也跟着关闭. # 因为当客户端关闭后,传过来的不知道是什么东西) print("用户传来了命令:%s"%client_cmd_re) # 打印输出用户传来的命令 cmd_re_str = str(client_cmd_re,'utf8') # 将用户传来的命令转换成str # 将命令执行,并获得执行结果 cmd_exec_result = subprocess.Popen(cmd_re_str,shell=True,stdout=subprocess.PIPE).stdout.read() if len(cmd_exec_result) == 0 : # 判断结果是不是为空,为空说明用户传过来的命令,本服务器无法执行. cmd_exec_result = bytes("命令 '{}' 后没有返回执行结果,请检查命令是否正确...".format(cmd_re_str),'utf8') # 提示命令不对 result_size = len(cmd_exec_result) # 计算本次获得到结果是多少字节 conn.send(bytes("CMD_RESULT_SIZE|{}".format(result_size),'utf8')) # 首先将本次将要发送的字符串的大小告诉客户端. conn.send(cmd_exec_result) #紧接着把执行结果发过去 conn.close()
07socket_client.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' 当服务端发送过来的字符串字节数过大,一次接收不完,客户端通过判断服务器端是不是发送完,来作为循环接收的依据. 如何判断服务器端是不是发送完成? 根据服务器端发来的大小,判断是否接收完成. 代码如下: ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) # sk.sendall(bytes("请求占领地球",'utf8')) # # server_reply = sk.recv(1024) # print(str(server_reply,'utf8')) while True: input_cmd = input("cmd:").strip() # 用户输入命令 if input_cmd == 'q':break # 用户如果输入的是q,退出输入命令的循环 if not input_cmd:continue # 用户如果直接按了回车,既为空,则进行下次循环 print(input_cmd) # 打印此次用户输入的命令 sk.send(bytes(input_cmd,'utf8')) # 将用户的命令通过socket发送给服务器端 server_ack_msg = sk.recv(100) cmd_res_msg = str(server_ack_msg,'utf8').split('|') # "CMD_RESULT_SIZE|{}".format(result_size),'utf8') # print(cmd_res_msg) if cmd_res_msg[0] == "CMD_RESULT_SIZE": # 判断你发来的这个是不是文件大小的标示 cmd_res_size = int(cmd_res_msg[1]) #如果是,把大小付给cmd_res_size变量 res = bytes('','utf8') recevied_size = 0 # 初始化字符串大小为0 while recevied_size < cmd_res_size: # 如果已接收的字符串大小小于服务器传过来的大小,则循环接收 data = sk.recv(50) # 接收数据 recevied_size += len(data) # 将数据累加 res += data # 累加统计已接收的数据 print(str(res,'utf8')) # 打印最终结果 sk.close()
到此为止,我们认为,socket接收大数据的功能已经实现。大功告成~~~,no,还有错误,当数据量大时,会出现 “socket编程中最大的一个坑 粘包 ”
我们看07socket_server.py中最后发送的代码
conn.send(bytes("CMD_RESULT_SIZE|{}".format(result_size),'utf8')) # 首先将本次将要发送的字符串的大小告诉客户端. conn.send(cmd_exec_result)
我们看到一连两次发送。这就会出现“socket编程中最大的一个坑 粘包 ”,原因是缓冲区的问题,你两次发送,缓冲区会把两次要发送的内容放到一起发送。
那么怎么解决呢?
两个方案:
1.在两次发送之间sleep(1) 间隔1秒,这样缓冲区就失效。但是这种方式low,如果并发大就会很慢。
2.发送完文件大小后,使用conn.recv(100)这个能阻塞的方法,获取客户端返回来已经接收到文件大小信息的确认信息。(当然客户端也要加返回给服务端的确认信息。)
最终代码如下:
08socket_server.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' socket学习,做成可以远程输入命令,客户端获得输出结果,如果你的命令输出结果很多,那么客户端得到的结 ''' import socket import subprocess ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.bind(ip_port) sk.listen(5) while True: print('server waiting ......') conn,addr = sk.accept() # client_data = conn.recv(1024) # print(str(client_data,'utf8')) # conn.send(bytes("不要回答,不要回答",'utf8')) while True: print("客户端{}已经连接,等待客户端传输命令... ...".format(conn.getpeername())) client_cmd_re = conn.recv(1024) if not client_cmd_re:break # 判断当,对方传过来的值为空时,直接退出(在客户端我已经判断用户输入不能为空,所以这里为空,只有一种可能,就是客户端关闭程序了.如果没有这句的break,会导致客户端一旦关闭,服务端也跟着关闭. # 因为当客户端关闭后,传过来的不知道是什么东西) print("用户传来了命令:%s"%client_cmd_re) # 打印输出用户传来的命令 cmd_re_str = str(client_cmd_re,'utf8') # 将用户传来的命令转换成str # 将命令执行,并获得执行结果 cmd_exec_result = subprocess.Popen(cmd_re_str,shell=True,stdout=subprocess.PIPE).stdout.read() if len(cmd_exec_result) == 0 : # 判断结果是不是为空,为空说明用户传过来的命令,本服务器无法执行. cmd_exec_result = bytes("命令 '{}' 后没有返回执行结果,请检查命令是否正确...".format(cmd_re_str),'utf8') # 提示命令不对 result_size = len(cmd_exec_result) # 计算本次获得到结果是多少字节 conn.send(bytes("CMD_RESULT_SIZE|{}".format(result_size),'utf8')) # 首先将本次将要发送的字符串的大小告诉客户端. client_ack = conn.recv(50) if str(client_ack,'utf8') == "CLIENT_READY_TO_RECV": conn.send(cmd_exec_result) conn.close()
08socket_client.py
#!/usr/bin/env python3.5 #__author__:'ted.zhou' ''' 当服务端发送过来的字符串字节数过大,一次接收不完,客户端通过判断服务器端是不是发送完,来作为循环接收的依据. 如何判断服务器端是不是发送完成? 根据服务器端发来的大小,判断是否接收完成. 代码如下: ''' import socket ip_port = ('127.0.0.1',9999) sk = socket.socket() sk.connect(ip_port) # sk.sendall(bytes("请求占领地球",'utf8')) # # server_reply = sk.recv(1024) # print(str(server_reply,'utf8')) while True: input_cmd = input("cmd:").strip() # 用户输入命令 if input_cmd == 'q':break # 用户如果输入的是q,退出输入命令的循环 if not input_cmd:continue # 用户如果直接按了回车,既为空,则进行下次循环 print(input_cmd) # 打印此次用户输入的命令 sk.send(bytes(input_cmd,'utf8')) # 将用户的命令通过socket发送给服务器端 server_ack_msg = sk.recv(100) cmd_res_msg = str(server_ack_msg,'utf8').split('|') # "CMD_RESULT_SIZE|{}".format(result_size),'utf8') # print(cmd_res_msg) if cmd_res_msg[0] == "CMD_RESULT_SIZE": # 判断你发来的这个是不是文件大小的标示 cmd_res_size = int(cmd_res_msg[1]) #如果是,把大小付给cmd_res_size变量 sk.send(b"CLIENT_READY_TO_RECV") res = bytes('','utf8') recevied_size = 0 # 初始化字符串大小为0 while recevied_size < cmd_res_size: # 如果已接收的字符串大小小于服务器传过来的大小,则循环接收 data = sk.recv(50) # 接收数据 recevied_size += len(data) # 将数据累加 res += data # 累加统计已接收的数据 print(str(res,'utf8')) # 打印最终结果 sk.close()