python学习之【16】网络编程
主题
- 客户端/服务器架构
- 套接字:通信终点
- 套接字地址
- 面向连接与无连接套接字
- Python中的网络编程
- SOCKET模块
- 套接字对象方法
- TCP/IP客户端和服务器
- UDP/IP客户端和服务器
- SocketServer模块
- Twisted框架介绍
- 相关模块
1.客户端服务器架构
客户<---->INTERNET<------->服务器。客户连上一个预先已知的服务器,提出自己的请求,发送必要的数据,然后就等待服务器返回的数据。
2.套接字:通信终点
套接字是一种具有"通信端点"概念的计算机网络数据结构。网络化的应用程序在开始任何通讯之前都需要创建套接字、就像电话插口一样,没有它就会完全没办法通信。Python只支持AF_UNIX,AF_NETLINK,AF_INET家族,由于我们只关心网络编程所以我们都只用AF_INET。
3.套接字地址
如果把套接字比作电话接口----------即通信的最底层结构,那主机与端口就像区号与电话号码的一对组合。有了电话还不够,你还要知道你要打电话给谁,往哪打。一个因特网地址由网络通信所必须的主机和端口组成,而且另一端一定要有人听才可以。
合法的端口号范围为0~65535,其中,小于1024的端口号为系统保留端口。
4.面向连接与无连接套接字
(1)面向连接
无论使用哪个地址家族,套接字的类型只有两种,面向连接和无连接。面向连接即是在通信前必须建立一条连接,就像跟朋友打电话一样。实现这种连接的主要协议就是传输控制协议(TCP),要创建TCP套接字就得在创建的时候指定套接字类型为SOCK_STREAM。TCP套接字采用SOCK_STREAM这个名字体现了它最为流套接字的特点,由于这些套接字使用网际协议(ip)来查找网络中的主机,所以这样形成的整个系统,一般会有两个协议(TCP和IP)名的组合来描述,即TCP/IP。
(2)无连接
即无需连接就可以进行通信。但这时,数据到达的顺序、可靠性及不重复性就无法保证了。数据是整个发送的,不会像面向连接的协议那样先被拆分成小块。
实现这种连接的主要协议就是用户数据报协议(UDP)。要创建UDP套接字就得在创建的时候指定套接字类型为SOCK_DGRAM。"datagram"是"数据报"。
5.Python中的网络编程
我们将使用Python中的socket模块,模块中的socket()函数用来创建套接字。要使用socket.socket()函数来创建套接字,语法如下:
socket.socket(socket_family,socket_type,protocol=0)
如前所述,Socket_family 不是 AF_VNIX 就是 AF_INET ,socket_type可以是SOCK_STREAM或是SOCK_DGRAM,protocol一般不填,默认为0。
6.SOCKET模块
例如创建一个TCP/IP的套接字,你要这样调用socket.socket()函数:
tcpSock = socket.socket(socket.AF_INET,socket.SOCKET_STREAM)
UDP/IP的套接字
udpSock = socket.socket(socket.AF_INET,socket.SOCKET_DGRAM)
由于socket模块中有太多的属性,我们在这里可以使用 “from module import *”的语句,我们就把socket模块里所有属性都带到我们的命名空间里了。
tcpSock = socket(AF_INET,SOCKET_STREAM)
7.套接字对象(内建)方法
函数 描述
服务器端套接字函数
- s.bind( ) 绑定地址(主机名,端口号对)到套接字
- s.listen( ) 开始TCP监听
- s.accept( ) 被动接受TCP客户端连接,(阻塞式)等待连接的到来。
- s.connect( ) 主动初始化TCP服务器连接。
- s.connect_ex( ) connect( )扩展版本,出错时返回错误码,而不是抛出异常。
- s,recv( ) 接受TCP数据。
- s.send( ) 发送TCP数据。
- s.sendall( ) 完整发送TCP数据。
- s.recvfrom( ) 接受UDP数据。
- s.sendto( ) 发送UDP数据。
- s.getpeername( ) 连接到当前套接字的远程的地址(TCP连接)
- s.getsockname( ) 当前套接字的地址。
- s.getsockopt( ) 返回当前套接字的参数。
- s.setsockopt( ) 设置指定套接字的参数。
- s.close( ) 关闭套接字。
- s.setblocking( ) 设置套接字的阻塞与非阻塞模式。
- s.settimeout( ) 设置阻塞套接字操作的超时时间。
- s.gettimeout( ) 得到阻塞套接字操作的超时时间。
- s.fileno( ) 套接字文件描述符。
- s.makefile( ) 创建一个与该套接字关联的文件对象。
TCP服务器设计伪代码:
ss = socket() #创建服务器套接字
ss.bind() #把地址绑定到套接字上
ss.listen() #监听连接
inf_loop: #服务器无线循环
cs = ss.accept() #接受客户端连接
comm_loop: #通信循环
cs.recv()/cs.send() #对话(接收与发送)
cs.close() #关闭客户端套接字
ss.close() #关闭服务器套接字(可选)
所有套接字都用socket.socket( )函数来创建。服务器需要"坐在某个端口上"等待请求。所以它们必须要"绑定"到一个本地的地址上。一个简单的(单线程)服务器会调用accept( )函数等待连接的到来。默认情况下,accept( )函数是阻塞式的,即程序在连接到来之前会处于挂起状态。套接字也支持非阻塞状态。
TCP时间戳服务器(tsTserv.py)
创建一个能接受客户端的消息,在消息前加一个时间戳后返回TCP服务器
from socket import * from time import ctime HOST = '' PORT = 21567 BUFSIZE = 1024 ADDR = (HOST,PORT) tcpSerSock = socket(AF_INET,SOCK_STREAM) tcpSerSock.bind( ADDR) tcpSerSock.listen(5) while True: print 'waiting for connection......' tcpCliSock,addr = tcpSerSock.accept( ) print '......connected from:',addr while True: data = tcpCliSock.recv(BUFSIZ) if not data: break tcpCliSock.send('[%s]%s' %(ctime(),data)) tcpCliSock.close( ) tcpSerSock.close()
创建TCP客户端
伪代码:
cs = socket( ) #创建客户端套接字
cs.connect( ) #尝试连接服务器
comm_loop:
cs.send( )/cs.recv( ) #对话(发送/接收)
cs.close( ) #关闭客户端套接字
TCP时间戳客户端(tsTclnt.py)
1 from socket import * 2 3 HOST = 'localhost' 4 PORT = 21567 5 BUFSIZ = 1024 6 ADDR = (HOST,PORT) 7 8 tcpCliSock = socket(AF_INET,SOCK_STREAM) 9 tcpCliSock.connect(ADDR) 10 11 while True: 12 data = raw_input('>') 13 if not data: 14 break 15 tcpCliSock.send(data) 16 data = tcpCliSock.recv(BUFSIZ) 17 if not data: 18 break 19 print data 20 21 tcpCliSock.close()
9.UDP/IP客户端和服务器
UDP时间戳服务器(tsUserv.py)
1 from socket import * 2 from time import ctime 3 4 HOST = '' 5 PORT = 21567 6 BUFSIZ = 1024 7 ADDR = (HOST,PORT) 8 9 udpSerSock = socket(AF_INET,SOCK_DGRAM) 10 udpSerSock.bind(ADDR) 11 12 while True: 13 print 'waiting for message......' 14 data,addr = udpSerSock.recvfrom(BUFSIZ) 15 udpSerSock.sendto('[%s] %s' % (ctime,data) ,addr) 16 print '......received from and returned to:',addr 17 18 udpSerSock.close()
创建一个UDP客户端
1 from socket import * 2 3 HOST = 'localhost' 4 PORT = "21567" 5 BUFSIZ = 1024 6 ADDR = (HOST,PORT) 7 8 udpCliSock = socket(AF_INET,SOCK_DGRAM) 9 10 while Trus: 11 data = raw_input('>') 12 if not data: 13 break 14 udpCliSock.sendto(data,ADDR) 15 data,ADDR = udpCliSock.recvfrom(BUFSIZ) 16 if not data: 17 break 18 print data 19 20 udpCliSock.close()
10.SocketServer模块
SocketServer 是标准库中一个高级别的模块。用于简化实现网络客户端与服务器所需的大量样板代码。该模块中,已经实现了一些可供使用的类。
SocketServer模块的类
类 | 描述 |
BaseServer | 包含服务器核心功能与混合(mix-in)类挂钩;这个类只用于派生,所以不会生成这个类的实例;可以考虑使用TCPServer和UDPServer |
TCPServer/UDPServer | 基本的网络同步TCP/UDP服务器 |
UnixStreamServer/UnixDatagramServer | 基本的基于文件同步TCP/UDP服务器 |
ForkingMixIn/ThreadingMixIn | 实现了核心的进程化或线程化的功能;作为混合类,与服务器类一并使用以提供一些异步特性;这个类不会直接实例化。 |
ThreadingTCPServer/ThreadingUDPServer | ThreadingMixIn和TCPServer/UDPServer的组合 |
BaseRequestHandler | 包含处理服务器请求的核心功能,这个类只用于派生,所以不会生成这个类的实例可以考虑使用StreamRequestHandler 或 DatagramRequestHandler |
StreamRequestHandler/DatagramRequestHandler | 用于TCP/UDP服务器处理工具 |
创建一个SocketServerTCP服务器
在代码中,先导入我们的服务器类,然后像之前一样定义主机常量。主机常量后就是我们的请求处理器类,然后是启动代码。
例 SocketServer 时间戳TCP服务器(TsTservss.py)
from SocketServer import (TCPServer as TCP,StreamRequsetHandler as SRH) from time import ctime HOST = '' PORT = 21567 ADDR = (HOST,PORT) class MyRequestHandler(SRH): def handle(self): print '......connected from:',self.client_address self.wfile.write('[%s]%s' % (ctime(),self.rfile.readline())) tcpServ = TCP(ADDR,MyRequestHandler) print 'waiting for connection...' tcpServ.serve_forever( )
创建SocketServerTCP客户端
1 from socket import * 2 3 HOST = 'localhost' 4 PORT = 21567 5 BUFSIZ = 1024 6 ADDR = (HOST,PORT) 7 8 while True: 9 tcpCliSock = socket(AF_INET,SOCK_STREAM) 10 tcpCliSock.connect(ADDR) 11 data = raw_input('>') 12 if not data: 13 break 14 tcpCliSock.send('%s\r\n' %data) 15 data = tcpCliSock.recv(BUFSIZ) 16 if not data: 17 break 18 print data.strip() 19 tcpCliSock.close() 20
11.Twisted框架介绍
Twsited是一个完全事件驱动的网络框架。它允许你使用和开发完全异步的网络应用程序和协议。需要安装它,系统中可以有:网络协议、线程、安全和认证、聊天/即时通讯、数据库管理、关系数据库集成、Web/internet、电子邮件、命令行参数、图形界面集成等。
创建一个Twisted Reactor TCP服务器
Twsited Reactor事件戳服务器(tsTservTW.py)
from twisted.internet import protocol,reactor
from time import ctime
PORT = 21567
class TSServProtocol(protocol.Protocol)
-----------------------
课后题
16-1. 套接字,面向连接与无连接有何区别?
答:socket第二个参数不同。面向连接即tcp通信函数为socket.socket(socket.AF_INET,socket.SOCK_STREAM),UDP通信函数第二个参数为SOCK_DGRAM。
16-2. 客户端/服务器架构。用你自己的语言描述这个架构。并给出几个例子?
答:服务器在某端口等待客户端的连接。怎么说呢 client<------------>internet<------------->server
16-3. 套接字。TCP和UDP中,哪一种服务器在接受连接后,把连接交给不同的套接字处理与客户端的通讯。
答:TCP。
16-4. 修改TCP(tsTclnt.py)和UDP(tsUclnt.py)客户端,让服务器的名字不要在代码里写死,要允许用户指定一个主机名和端口,只有在两个值都没有输入的时候才使用默认值。
答:修改后的tsTclnt.py
1 from socket import * 2 from time import ctime 3 4 5 HOST = '' 6 PORT = 21567 7 BUFSIZ = 1024 8 ADDR = (HOST,PORT) 9 10 11 tcpSerSock = socket(AF_INET,SOCK_STREAM) 12 tcpSerSock.bind(ADDR) 13 tcpSerSock.listen(5) 14 15 16 while True: 17 print 'waitiing for connection...' 18 tcpCliSock,addr = tcpSerSock.accept() 19 print '...connected from:',addr 20 21 22 while True: 23 data = tcpCliSock.recv(BUFSIZ) 24 if not data: 25 break 26 tcpCliSock.send('[%s] %s' % (ctime(),data)) 27 28 tcpSerSock.close() 29 30
16-5. 网络互联和套接字。找到《python library reference》 示例tcp客户端/服务器程序,实现它并让它运行起来。先运行服务器,然后是客户端。http://www.python.org/doc/current/lib/Socket_Example.html
你认为这个服务器太无聊,决定要修改服务器,让它能识别以下命令:
data 服务器将返回它的当前时间,即time.ctime(time.time())
os 得到操作系统的信息(os.name)
ls 得到当前目录的文件列表
做这个作业的时候,你不一定要有网络------你的机器可以自己通讯。注:在服务器退出后,要清除绑定后才能再次运行,否则有可能得碰到‘端口已经被使用’的错误信息,操作系统一般会在5分钟内清除绑定。所以请耐心等待。
服务器端
1 from socket import * 2 import time,os 3 4 5 HOST = '' 6 PORT = 21567 7 BUFSIZ = 1024 8 ADDR = (HOST,PORT) 9 10 11 tcpSerSock = socket(AF_INET,SOCK_STREAM) 12 tcpSerSock.bind(ADDR) 13 tcpSerSock.listen(5) 14 15 16 while True: 17 print 'waitiing for connection...' 18 tcpCliSock,addr = tcpSerSock.accept() 19 print '...connected from:',addr 20 21 while True: 22 data = tcpCliSock.recv(BUFSIZ) 23 data = data.strip() 24 if not data: 25 break 26 if data == "date": 27 tcpCliSock.send('%s' % (time.ctime(time.time()))) 28 elif data == "os": 29 tcpCliSock.send('%s' % (os.name)) 30 elif data == "ls": 31 tcpCliSock.send('%s' % (os.listdir(os.curdir))) 32 else: 33 tcpCliSock.send('[%s] %s' % (time.ctime(),data)) 34 35 36 tcpSerSock.close()
16-6日期时间服务。使用socket.getservbyname()函数得到UDP协议中"daytime"服务所对应的端口。请参考getservbyname()函数的文档,查阅如何使用的详细语法。现在,写一个程序发送一个随便什么数据过去,等待回答。一旦你收到了服务器的信息,显示到屏幕上去。
答:服务端
1 from socket import * 2 from time import ctime 3 4 5 HOST = "localhost" 6 7 PORT = getservbyname("daytime","udp") 8 9 BUFSIZ = 1024 10 11 ADDR = (HOST,PORT) 12 13 udpSerSock = socket(AF_INET,SOCK_DGRAM) 14 15 udpSerSock.bind(ADDR) 16 17 while True: 18 print "waiting for message..." 19 data,addr = udpSerSock.recvfrom(BUFSIZ) 20 udpSerSock.sendto("[%s]%s" % (ctime(), data),addr) 21 print "...received from and returned to:",addr 22 23 24 udpSerSock.close 25
客户端
1 from socket import * 2 from time import ctime 3 4 5 HOST = "localhost" 6 PORT = getservbyname("daytime","udp") 7 8 BUFSIZ = 1024 9 10 ADDR = (HOST,PORT) 11 12 udpCliSock = socket(AF_INET,SOCK_DGRAM) 13 14 15 while True: 16 data = raw_input(">") 17 if not data: 18 break 19 udpCliSock.sendto(data,ADDR) 20 data,ADDR = udpCliSock.recvfrom(BUFSIZ) 21 if not data: 22 break 23 print data 24 25 tcpCliSock.close()
16-7.半双工聊天。创建一个简单的半双工聊天程序。“半双工”的意思是当创建一个连接,服务启动的时候,只有一个人可以打字,另一个人只有在等到消息通知他输入消息时才能说话。一旦消息发送出去后,要等到有回复了才能发送下一条消息。一个人是服务端,另一个人是客户端。
客户端
from socket import * HOST = "localhost" PORT = 23346 BUFSIZ = 1024 ADDR = (HOST,PORT) tcpCliSock = socket(AF_INET,SOCK_STREAM) tcpCliSock.connect(ADDR) while True: data = raw_input("client>") if not data: continue tcpCliSock.send(data) while True: data = tcpCliSock.recv(BUFSIZ) if not data: continue else: print "server>",data break tcpCliSock.close()
服务端
1 from socket import * 2 3 HOST = "localhost" 4 PORT = 23346 5 BUFSIZ = 1024 6 ADDR = (HOST,PORT) 7 8 9 tcpServSock = socket(AF_INET,SOCK_STREAM) 10 tcpServSock.bind(ADDR) 11 tcpServSock.listen(5) 12 13 14 15 while True: 16 print "waiting for connection..." 17 tcpCliSock,addr = tcpServSock.accept() 18 print "connected from:",addr 19 20 while True: 21 data = tcpCliSock.recv(BUFSIZ) 22 if not data: 23 continue 24 print "client>",data 25 data = raw_input("server>") 26 tcpCliSock.send(data) 27 28 tcpServSock.close() 29
---------------------------------------------------------------------
16-8 全双工聊天。修改你刚才的程序,改成全双工,即两个人可以独立地发送和接收消息。
答:
服务端
1 #!/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 import socket,traceback,os 4 from threading import * 5 6 host = 'localhost' 7 port = 51423 #监听所有的接口 8 9 #接受消息的线程 10 def handlerecv(clientsock): 11 print "New child",currentThread().getName() 12 print "Got connection from",clientsock.getpeername() 13 while True: 14 data = clientsock.recv(4096) 15 if not len(data): 16 break 17 print data 18 clientsock.close() 19 20 #发送消息的线程 21 def handlesend(clientsock): 22 while True: 23 data = raw_input(">") 24 data = data + "\n"; #加上换行,好看一点。 25 clientsock.sendall(data) 26 #关闭连接 27 clientsock.close() 28 29 #建立套接字 30 s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 31 s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 32 s.bind((host,port)) 33 s.listen(1) 34 35 while True: 36 try: 37 clientsock,clientaddr = s.accept() 38 except KeyboardInterrupt: 39 raise 40 except: 41 traceback.print_exc() 42 continue 43 44 t = Thread(target = handlerecv,args=[clientsock]) 45 t.setDaemon(1) 46 t.start() 47 48 r = Thread(target = handlesend,args=[clientsock]) 49 r.setDaemon(1) 50 r.start() 51 52 53 54
客户端
1 #!/usr/bin/env python 2 #-*-coding:utf-8-*- 3 4 from socket import * 5 import sys 6 from threading import * 7 8 if(len(sys.argv) < 3): 9 HOST = 'localhost' 10 PORT = 51423 11 else: 12 HOST = sys.argv[1] 13 PORT = int(sys.argv[2]) 14 15 BUFSIZ = 1024 16 ADDR = (HOST,PORT) 17 18 def handlesend(tcpCliSock): 19 while True: 20 sdata = raw_input('> ') 21 if not sdata: 22 break 23 tcpCliSock.send(sdata) 24 25 tcpCliSock.close() 26 27 tcpCliSock = socket(AF_INET,SOCK_STREAM) 28 tcpCliSock.connect(ADDR) 29 30 #建立发送消息的线程 31 s = Thread(target = handlesend,args=[tcpCliSock]) 32 s.setDaemon(1) 33 s.start() 34 35 while True: 36 rdata = tcpCliSock.recv(BUFSIZ) 37 if not rdata: 38 break 39 print rdata 40 tcpCliSock.close()
------------------------------------------------------------------
16-9----10 多用户全双工聊天。再次修改你的程序,把聊天服务改成支持多用户版本。(扩展功能多人,多房间,多线程,全双工)
服务端 cs.py
1 #!/usr/bin/env python 2 # _*_ coding: utf8 _*_ 3 4 from socket import * 5 from time import ctime 6 import threading 7 from string import split 8 9 HOST = '' 10 PORT = 21567 11 BUFSIZE = 1024 12 ADDR = (HOST, PORT) 13 14 def Deal(sck, username, room): 15 while True: 16 data = sck.recv(BUFSIZE) 17 for i in clients[room].iterkeys(): 18 if i <> username: 19 if data <> "quit": 20 clients[room][i].send("[%s] %s: %s" %(ctime(), username, data)) 21 else: 22 clients[room][i].send("用户%s在%s退出房间%s" %(username, ctime(), room )) 23 if data == "quit": 24 del clients[room][username] 25 sck.send(data) 26 sck.close() 27 break 28 29 30 chatSerSock = socket(AF_INET, SOCK_STREAM) 31 chatSerSock.bind(ADDR) 32 chatSerSock.listen(5) 33 34 clients = {"":{},} 35 36 while True: 37 print 'waiting for connection...' 38 chatCliSock, addr = chatSerSock.accept() 39 print "...connected romt: ", addr 40 data = chatCliSock.recv(BUFSIZE) 41 username, room = split(data) 42 print username 43 if not clients.has_key(room): 44 clients[room] = {} 45 if clients[room].has_key(username): 46 chatCliSock.send("reuse") 47 chatCliSock.close() 48 else: 49 chatCliSock.send("success") 50 clients[room][username] = chatCliSock 51 t = threading.Thread(target=Deal, args=(chatCliSock, username, room)) 52 t.start() 53 54 chatSerSock.close()
客户端 cc.py
1 #!/usr/bin/env python 2 # _*_ coding: utf8 _*_ 3 4 from socket import * 5 from time import ctime 6 import threading 7 import random 8 from sys import argv, exit, stdout 9 from getopt import gnu_getopt, GetoptError 10 11 12 13 14 help_info = ["cs.py [ -h | --help | -u | --username] username", 15 "\t-h or --help\t显示帮助信息", 16 "\t-u or --username\指定用户名", 17 "\t-r or --room\t指定房间"] 18 def help(): 19 for i in help_info: 20 print i 21 22 23 24 def Send(sck, test): 25 while True: 26 data = raw_input('>') 27 sck.send(data) 28 if data == "quit": 29 break 30 def Recieve(sck, test): 31 while True: 32 data = sck.recv(BUFSIZ) 33 if data == "quit": 34 sck.close() 35 break 36 str = "\n" + data + "\n>" 37 stdout.write(str) 38 39 HOST = 'localhost' 40 PORT= 21567 41 BUFSIZ = 1024 42 ADDR = (HOST, PORT) 43 threads = [] 44 45 if __name__ == "__main__": 46 # 解析命令行参数 47 try: 48 opts, args = gnu_getopt(argv[1:], "hu:r:", ["help", "username=", "room="]) 49 except GetoptError, err: 50 print str(err) 51 help() 52 exit(2) 53 username = "" 54 room = "" 55 for o, a in opts: 56 if o in ("-h", "--help"): 57 help() 58 exit(0) 59 elif o in ("-u", "--username"): 60 username = a 61 elif o in ("-r", "--room"): 62 room = a 63 else: 64 print "未知选项" 65 help() 66 exit(2) 67 if not username or not room: 68 help() 69 exit(2) 70 chatCliSock = socket(AF_INET, SOCK_STREAM) 71 chatCliSock.connect(ADDR) 72 chatCliSock.send("%s %s" %(username, room)) 73 data = chatCliSock.recv(BUFSIZ) 74 if data == "reuse": 75 print "用户%s已登录房间%s" %(username, room) 76 raw_input() 77 exit(1) 78 elif data == "success": 79 print "用户%s成功登录房间%s" %(username, room) 80 t = threading.Thread(target=Send, args = (chatCliSock, None)) 81 threads.append(t) 82 t = threading.Thread(target=Recieve, args = (chatCliSock, None)) 83 threads.append(t) 84 for i in range(len(threads)): 85 threads[i].start() 86 threads[0].join()