一、socket()模塊函數
要使用socket.socket()函數來創建套接字,其語法如下:
socket(socket_family,socket_type,protocol=0)
如上所述,scoket_family不是AF_UNIX就是AF_INET,scoket_type可以是SOCK_STREAM或SOCK_DGRAM,protocol一般不填,默認值為0.
創建一個TCP/IP套接字,你要這樣調用socket.socket():
tcpsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
同樣的,創建一個UDP/IP的套接字,你要這樣:
udpsock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
由於socket有太多屬性,我們一般使用from import socket * 語句,將所有屬性導入命名空間。
二、套接字對象內建方法
下面是一些套接字對象常用的方法。
| 函數 | 描述 |
| 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.settimeout() | 設置阻塞套接字操作的超時時間 |
| s.gettimeout() | 得到阻塞套接字操作的超時時間 |
| s.setblocking() | 設置套接字的阻塞與非阻塞模式 |
| 函數 | 描述 |
| s.fileno() | 套接字的文件描述符 |
| s.makefile() | 創建一個與該套接字關聯的文件對象 |
提示:在運行網絡應用程序時,最好在不同的電腦上執行服務器和客戶端的程序。
三、創建TCP服務器和TCP客戶端
根據上面的介紹,現在我們應該能創建一個完整的通信模型了。下面是理論上的偽代碼:
1.套接字理論模型
先來創建一個TCP服務器
#創建一個TCP服務器 ss = socket() #創建服務器套接字 ss.bind() #把地址綁定到套接字上 ss.listen() #監聽連接 inf_loop: #服務器無線循環 cone,addr = ss.accept() #接收客戶端連接 comm_loop: #通信循環 cone.recv()/cs.send() #對話(接受與發送) cone.close() #關閉客戶端套接字 ss.close() #關閉服務器套接字(可選)
所有的套接字都用socket.socket()函數來創建。服務器需要“坐在某個端口上”等待請求。所以它們必須要綁定到一個本地的地址上。
由於TCP是一個面向連接的通信系統,在TCP服務器可以開始工作之前,要先完成一些設置。
TCP服務器必須監聽(進來的)連接,設置完成之后,服務器就可以進入無線循環了。
一個簡單的(單線程的)服務器會調用accept()函數等待連接的到來,
默認情況下,accept()函數是阻塞式的,即程序在連接到來之前會處於掛起狀態。套接字也支持非阻塞模式。
一旦接收到一個連接,accept·()函數就會返回一個單獨的客戶端套接字用於后續的通信。
使用新的客戶端套接字就像把客戶的電話轉給一個客戶服務人員。當一個客戶打電話進來的時候,總機接了電話,然后把電話轉到合適的人那里來處理客戶的需求。
這樣就可以空出主機,也就是最初的那個服務器套接字。
當客戶端連接關閉后,服務器繼續等待下一個客戶端的連接。代碼的最后一行會把服務器套接字關閉,由於是無限循環也許用不到。
再來創建一個TCP客戶端
#創建一個TCP客戶端 ss = socket() #創建一個客戶端套接字 ss.connect() #嘗試連接服務器 comm_loop: #通信循環 cs.send()/cs.recv() #對話(接受或發送) cs.close() #關閉客戶端套接字
在客戶端有了套接字之后,馬上就可以調用connect()函數去連接服務器。連接建立之后,就可以與服務器開始對話了,對話結束后,客戶端就可以關閉套接字,結束連接
2.建立一個單一的連接
#服務器端 from socket import * cs = socket(AF_INET,SOCK_STREAM) cs.bind(('127.0.0.1',8888)) cs.listen(5) print("Wait for......") anne,addr = cs.accept() print(anne) print(addr) anne.close()
#客戶端
from socket import * cl = socket(AF_INET,SOCK_STREAM) cl.connect(("127.0.0.1",8888)) cl.send("Hello,world".encode("utf-8")) cl.close()
先啟動服務器:
Wait for...... #accept處於等待狀態
然后執行客戶端,看服務器端的變化:
<socket.socket fd=384, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888), raddr=('127.0.0.1', 54883)> ('127.0.0.1', 54883) #客戶端IP地址和端口號
上面的的代碼有點單一,有多個客戶端同時訪問該如何?這就該用到后面的多線程,稍后會講,這里有另外的折中代碼,可以一直訪問,雖然一次只能訪問一個。
#服務器端 from socket import * cs = socket(AF_INET,SOCK_STREAM) cs.bind(("127.0.0.1",8888)) cs.listen(5) print("Have Listen") while True: cone,addr = cs.accept() while True: data = cone.recv(1024) if len(data) == 0:break #如果收到TCP消息,則關閉客戶端套接字 print(data.decode("utf-8")) cone.send(data.upper()) cone.close() cs.close() #客戶端 from socket import * cs = socket(AF_INET,SOCK_STREAM) cs.connect(("127.0.0.1",8888)) while True: ssg = input(">>>").strip() if not ssg:continue #避免空格造成的停頓 cs.send(ssg.encode("utf-8")) #發 data = cs.recv(1024) print(data.decode("utf-8")) #收 cs .close()
下面是在linux下的版本測試:
#服務端 #!/usr/bin/env python #coding:utf-8 from socket import * import time HOST = '192.168.43.131' PORT = 8808 BUFSIZ = 1024 ADDR = (HOST,PORT) tcpser = socket(AF_INET,SOCK_STREAM) tcpser.bind(ADDR) tcpser.listen(5) while True: print "等待連接......" anne,addr = tcpser.accept() print "...連接:",addr while True: data = anne.recv(BUFSIZ) if not data: break anne.send('[%s] %s' % (time.strftime('%c'),data)) anne.close() tcpser.close() #客戶端 #!/usr/bin/env python from socket import * HOST = '192.168.43.131' PORT = 8088 BUFSIZ = 1024 ADDR = (HOST,PORT) tcpcli = socket(AF_INET,SOCK_STREAM) tcpcli.connect(ADDR) while True: data = input(">>>") if not data: continue tcpcli.send(data.encode("utf-8")) data = tcpcli.recv(BUFSIZ) if not data: break print(data.decode("utf-8")) tcpcli.close()
四、創建UDP服務器和UDP客戶端
由於UDP服務器不是面向連接的,所以不用像TCP服務器那樣做那么多設置工作。
創建一個UDP服務器
#創建UDP服務器 ss = socket() #創建一個服務器套接字 ss.bind() #綁定服務器套接字 inf_loop: #服務器無限循環 cs = ss.recvfrom()/ss.sendto() #對話(接收與發送) ss.close() #關閉服務器套接字
從偽代碼中可以看出,使用的還是那套先創建套接字然后綁定到本地地址(主機/端口)的方法,無限循環中包含了從客戶接受消息。
創建一個TCP服務器
#創建一個UDP服務器 cs = socket() #創建客戶端套接字 comm_loop: #通訊循環 cs.sendto()/cs.recvfrom() #對話(發送/接收) cs.close() #關閉客戶端套接字
創建一個真實的案例:
#創建一個服務器 from socket import * HOST = "127.0.0.1" PORT = 8989 BUFSIZ = 1024 ADDR = (HOST,PORT) udpser = socket(AF_INET,SOCK_DGRAM) udpser.bind(ADDR) while True: print("等待請求......") conn,addr = udpser.recvfrom(BUFSIZ) #接收到的消息無需轉交 udpser.sendto( conn.upper(),addr) #需要的話返回一個結果就可以了 print("...來自",addr) udpser.close()
UDP和TCP服務器的另一個重要的區別是,由於數據報套接字是無連接的,所以無法把客戶端連接交給另外的套接字進行后續的通訊。
這些服務器只是接收消息,需要的話,給客戶端返回一個結果就可以了。
#創建一個客戶端服務器 from socket import * HOST = "127.0.0.1" PORT = 8989 BUFSIZ = 1024 ADDR = (HOST,PORT) udpcli = socket(AF_INET,SOCK_DGRAM) while True: data = input(">>>") if not data: break udpcli.sendto(data.encode("utf-8"),ADDR) data,ADDR = udpcli.recvfrom(BUFSIZ) if not data: continue print(data.decode("utf-8")) udpcli.close()
UDP客戶端的循環基本上與TCP客戶端的完全一樣。唯一的區別就是,我們不用先去跟UDP服務器建立連接,而是直接把消息發送出去,然后等待服務器的回復。
還可以,創建多個客戶端,UDP不同於TCP需要建立連接。
#服務端 from socket import * server = socket(AF_INET,SOCK_DGRAM) server.bind(('127.0.0.1',9100)) while True: conn,addr = server.recvfrom(1024) print("訪問來自%s,端口號是:%s" % (addr[0],addr[1])) server.sendto(conn.upper(),addr) #返回消息的時候,必須指定端口號和ip #客戶端1 from socket import * client = socket(AF_INET,SOCK_DGRAM) while True: data = input(">>>") #發送空格也行,不會報錯,一次發送,也不會占用資源 client.sendto(data.encode("utf-8"),('127.0.0.1',9100)) conn,addr = client.recvfrom(1024) print(conn.decode('utf-8')) #客戶端2 from socket import * client = socket(AF_INET,SOCK_DGRAM) while True: data = input(">>>") client.sendto(data.encode("utf-8"),('127.0.0.1',9100)) conn,addr = client.recvfrom(1024) print(conn.decode('utf-8'))
執行結果:
訪問來自127.0.0.1,端口號是:60715
訪問來自127.0.0.1,端口號是:60716
小結:
總的來說,UDP和TCP服務的流程相同,有兩點:UDP無需提前連接直接發送消息,UDP服務器無法把客戶端的連接轉交出去。
