本文共 9459 字,大约阅读时间需要 31 分钟。
服务端的例子:
import socketserver = socket.socket()server.bind(('localhost',11111))server.listen()print("监听已经开始")count = 0# 加个计数器,服务3次后停止服务while count<3: # accept是等待连接请求,所以在没有客户端连接的时候,希望回到这里 conn,addr = server.accept() print("发现连接请求:\n%s\n%s"%(conn,addr)) # 持续接收数据,发回给客户端 while True: data = conn.recv(1024) if not data: break # 这样可以正常退出循环,没有这句客户端断开后会报错 print("recv:",data.decode("utf-8")) conn.send("收到:".encode("utf-8") + data) print("断开与 %s 的连接,再次开始监听等待"%str(addr)) count += 1print("停止服务")server.close()
实例化一个socket
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)一般用IPv4的TCP协议,所以全部用默认参数就好了,也就是socke.socket()
。前2个参数比较重要,其他选项如下。 Socket Families(地址簇):
Socket Types:
socket.bind('localhost,9999')
socket.listen()
conn,addr = server.accept()
上面4步后,连接已经建立,就可以收发数据了。
conn.recv(1024)
:接收数据。参数是一次接收的字节数(Byte),这个数字不要超过8192(官方建议)。虽然可以更大,但是还是按照建议的来。按经验,一般都是设成2的N次方。recv默认是阻塞的,如果没有发来数据,就会一直等着。如果客户端断开,就会收到空数据。可以用if not data: break
可以在客户端断开后退出循环conn.send()
:conn.sendall()
: 客户端的例子:
import socketclient = socket.socket()client.connect(('localhost',11111))msg = input(">>:")# 把input的内容持续发送给服务器,如果发送空内容,就不发送直接跳出循环while msg: client.send(msg.encode("utf-8")) # 发送数据要转码成bytes类型 data = client.recv(1024) # 接收服务器端的回复 print('recv:',data.decode("utf-8")) # 打印出回复的内容 msg = input(">>:")else: input("准备断开连接,现在服务端还没断开\n" "回车后客户端close,服务端也同时close")client.close()
客户端具体就3步:
一、 实例化:定义好协议,实例化socket,同服务端一样client = socket.socket()
二、 连接:绑定套接字,请求连接服务器 client.connect(('localhost',11111))
三、 收发数据:与服务端通信,通服务端一样 就是客户端通过socket将命令发送给服务端
服务端通过os模块或者subprocess模块执行命令,并且把执行结果(最好还要有标准错误)再通过socket返回给客户端例子课上讲的比较简单,上次笔记里有,不过并不包括错误的返回问题:接收数据的命令 conn.recv(1024)
必须要有参数,而且这个参数也不能无限大,前面已经讲过。如果一次要接收的数据大于这个参数,就需要多次接收。但是我们现在并没有方法判断几次才能接收完,并且如果进入了recv但是又没有新数据进来,这时候就会出现阻塞。所以多recv一次会阻塞,少recv一次则收不全数据,必须要通过已经接收到的数据来判断出是否接收完成。
str1 = "abcDEF"print(len(str1))print(len(str1.encode('utf-8'))) # 默认编码应该就是utf-8。这个参数可省略str2 = '你好'print(len(str2))print(len(str2.encode('utf-8'))) # utf-8一个中文字占3个字节print(len(str2.encode('gbk'))) # unicode一个中文字占2个字节
如果发送方连续发送这个两个包出去,接收方一次接收过来,把两个包当做了一个包处理了,就会出现问题。
粘包出现的原因:解决办法:还是从编程上来避免粘包的出现。就是一端发送了一个包后,不要再发包了,而是先等待接收。而另外一端在接收到包之后,必须给一个答复。一定要一收一发交替出现。不要连续send两个包,中间要插入一次等待回复,然后才能发第二个包。
详细讲:一次发送和接收的数据有字节数的限制,对于大数据无法一次发送和接收完。所以发送方和接收发都得有一个循环把包完全发送和接收,并且包本身要带一个信息让接收方通过接收到的内容判断一个包已经接收完毕。多一次recv会进入阻塞,而少一次recv会导致包无法接收完全并且还有残留的数据在缓冲区里,导致下一次接收的数据也有错误。发送数据可以偷个懒,使用sendall方法。但是接收数据只能用循环接收的方式,并且每次recv都要判断一下是否收完了。判断一个包接收完毕的方法可以有很多,推荐的是包头先带上包大小的信息,也可以在包结尾加上特定的字符串标记。具体后面再讲,或者不讲了。一个完整的包接收完毕,无论接收方是否需要回复,编程设计上都应该让接收方回复一个包。回包表示接收方和发送方的缓冲区此时都是空的了,所以就避免粘包的情况。至于插入一个回包而产生的等待,在极端场景下可能会有效率问题吧,虽然一般情况下可能也感受不到。除了上面说的,还可以根据包头的数据大小的信息,让接收方一次只接收正好的字节数。如果有剩下的数据则留在缓冲区里下次再收,避免粘包。课上就是上面两种方法结合使用。应该方法也不止这些,不同的场景使用合适的方法,具体情况还要具体分析。socket无法支持多并发,所以前面的都只是铺垫,现在要再来学学SocketServer。
socketserver模块主要的作用是,简化网络服务器的开发。创建一个SocketServer的步骤:服务端的例子:
import socketserver# 第一步:创建一个处理请求的类class MyTCPHandler(socketserver.BaseRequestHandler): # 第一步:重构handle方法 def handle(self): # 每次收到一个连接请求。就会开始执行handle方法 print("收到客户端连接请求:",self.client_address) # 接收数据,self.request是连接进来的套接字 self.data = self.request.recv(1024) # 打印收到的数据 print(self.data) # 把收到的内容转换成全大写然后发回去 self.request.sendall(self.data.upper())if __name__ == "__main__": HOST, PORT = "localhost", 9999 # 第二步:实例化,第一个参数是TCP的地址簇,第二个参数是第一步建的类 server = socketserver.TCPServer((HOST, PORT), MyTCPHandler) # 第三部:开启监听 # 如果是request方法,一个客户请求处理完毕后就会就会继续往后执行 #server.handle_request() # forever方法,一个客户断开后,程序不会向下执行,而是继续等待下一个客户的请求 # 执行到这里服务器就是一直执行的状态,直到你Ctrl-C中断程序 server.serve_forever() # 所有的交互都是在handle方法里,所以之后就是执行handle中的程序 print("运行结束")
和客户端所有的交互都是在handle方法里写的,上面例子中的handle方法比较简单。就是收一次数据,然后再回一条,此时handle执行结束。返回主函数的 server.serve_forever()
继续等待下一次请求。
import socketdef run(): ''' 1 和服务器建立连接 2 等待用户输入,如果输入为空就退出 3 将用户输入的数据转成bytes发送 4 接收回复的数据,然后打印出来 5 返回第2步,继续让用户输入下一条信息 ''' client = socket.socket() client.connect(('localhost',9999)) msg = input(">>:") while msg: client.send(msg.encode("utf-8")) print('发送:',msg.encode("utf-8")) data = client.recv(1024) print('recv:',data.decode("utf-8")) msg = input(">>:") else: print("主动断开连接...") client.close()if __name__ == "__main__": run()
现在可以测试了,开启服务端和客户端,然后客户端输入消息发送。这里每次客户端只能发送一条消息,再要发送一条就会报错。因为此时服务端已经将连接中断了。这是因为服务端的handle中顺序执行完每一条语句后就会返回主函数的 server.serve_forever()
等待下一个连接请求。如果想要可以多次交互,需要在handle里写一个循环。
server.serve_forever()
的效果 创建socketserver的第一步,创建的类中除了要重构handle方法外,还有两个方法也可以选择新的重构它们。
setup()
:在 handle()
之前执行,可以用来执行所需的初始化操作。默认什么也不做。finish()
:在 handle()
之后执行,可以用来执行所需的任何清理操作。默认什么也不做。如果 setup()
中有抛出异常,该函数将不会被调用。上面的两个方法都是可选的,其实代码写在handle方法里应该也一样,不过如果有初始化或者清理的操作,写到对应的方法里面,结构应该是更加的清晰。后面一起举例。 这里改写一下handle方法,加入一个while循环,实现多次交互,并且检测如果客户端断开就跳出循环返回主程序。顺便加上前面的setup和finish方法
import socketserverclass MyTCPHandler(socketserver.BaseRequestHandler): def setup(self): print("This is in setup") def handle(self): print("收到客户端连接请求:",self.client_address) while True: self.data = self.request.recv(1024) # 如果客户端断开连接,就退出循环,最后会返回主程序 if not self.data: break print(self.data) self.request.sendall(self.data.upper()) print("客户端断开了连接") def finish(self): print('This is in finish')if __name__ == "__main__": HOST, PORT = "localhost", 9999 server = socketserver.TCPServer((HOST, PORT), MyTCPHandler) print("准备监听,等待用户接入") server.serve_forever() print("运行结束")
这里handle的代码和之前socket的服务的的代码是一样的。退出连接的检测也是一样的:如果客户的断开,这里recv获取的数据就会得到空,然后我们break。客户端如果吧断开,recv如果没有数据,只会继续阻塞在这一步,等待缓冲区进来数据。
到这里就和socket里基本一样了,差别只是在socketserver是修改handle方法。上面已经测试过了,目前仍然还是不支持多并发,一次只能连接一个客户端。因为我们调用的方法不对。
要实现多并发只需要使用 socketserver.ThreadingTCPServer
替代 socketserver.TCPServer
就可以了。这个是通过多线程实现的,有关多线程以后会学。不过目前只需要知道socket的多并发是通过多线程来实现的就可以了。
import socketserverclass MyTCPHandler(socketserver.BaseRequestHandler): def handle(self): # 现在可以有多个客户端同时接入了,每次print都带上客户端连接信息 print("收到客户端连接请求:",self.client_address) while True: self.data = self.request.recv(1024) if not self.data: break print(self.client_address,':',self.data) self.request.sendall(self.data.upper()) print(self.client_address,':',"客户端断开了连接")if __name__ == "__main__": HOST, PORT = "localhost", 9999 # 只要修改这里调用的方法就可以了 server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler) print("准备监听,等待用户接入") server.serve_forever() print("运行结束")
客户端还是老样子,现在可以多开几个客户端同时接入试一下了。
除了用多线程方法外,也可以通过多进程的方法来实现多并发。同样是换个命令 socketserver.ForkingTCPServer
就好了,不过windownds上用不了。这个可以去源码里看一下,文件是哪个,可以用 __file__
属性查到文件位置
import socketserverprint(socketserver.__file__)
现在知道了 \lib\socketserver.py ,源码就在这个位置。然后开头的注释后是一堆模块的导入。
import socketimport selectorsimport osimport errnoimport systry: import threadingexcept ImportError: import dummy_threading as threadingfrom io import BufferedIOBasefrom time import monotonic as time__all__ = ["BaseServer", "TCPServer", "UDPServer", "ThreadingUDPServer", "ThreadingTCPServer", "BaseRequestHandler", "StreamRequestHandler", "DatagramRequestHandler", "ThreadingMixIn"]if hasattr(os, "fork"): __all__.extend(["ForkingUDPServer","ForkingTCPServer", "ForkingMixIn"])if hasattr(socket, "AF_UNIX"): __all__.extend(["UnixStreamServer","UnixDatagramServer", "ThreadingUnixStreamServer", "ThreadingUnixDatagramServer"])
这里Forking的几个方法导入之前,先确认了当前系统是否有fork这个属性。试了一下确实没有。所以windows上用不了Foring的几个方法。但是多并发,之前用多线程的方法已经实现了,如果有别的系统可以用 hasattr(os,"fork")
看一下,Linux应该是支持的。
既然已经打开了源码文件,可以看到里面定义了很多方法。比如TCPServer类中的:
def server_close(self): """Called to clean-up the server. May be overridden. """ self.socket.close()
还有的甚至什么都没有直接一句pass,在BaseServer类中有很多,比如:
def server_activate(self): """Called by constructor to activate the server. May be overridden. """ pass
这些方法都是在别的过程中可能被调用到的,比如上面的方法会在实例化的时候再构造函数中被调用。我们也可以运用之前学习的面向对象的方法来重构这些方法,实现一些别的功能或需求。
高级FTP服务器开发:
转载于:https://blog.51cto.com/steed/2050273