轉一篇 Python Socket 程式設計的文章
Socket是什麼呢?
socket起源於Unix,而Unix/Linux基本哲學之一就是“一切皆檔案”,對於檔案用【開啟】【讀寫】【關閉】模式來操作。socket就是該模式的一個實現,socket即是一種特殊的檔案,一些socket函式就是對其進行的操作(讀/寫IO、開啟、關閉)
基本上,Socket 是任何一種計算機網路通訊中最基礎的內容。例如當你在瀏覽器位址列中輸入 http://www.cnblogs.com/時,你會開啟一個套接字,然後連線到 http://www.cnblogs.com/並讀取響應的頁面然後然後顯示出來。而其他一些聊天客戶端如 gtalk 和 skype 也是類似。任何網路通訊都是通過 Socket 來完成的。
Python 官方關於 Socket 的函式請看http://docs.python.org/library/socket.html
socket和file的區別:
1、file模組是針對某個指定檔案進行【開啟】【讀寫】【關閉】
2、socket模組是針對 伺服器端 和 客戶端Socket 進行【開啟】【讀寫】【關閉】
Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。
你會使用它們嗎?
前人已經給我們做了好多的事了,網路間的通訊也就簡單了許多,但畢竟還是有挺多工作要做的。以前聽到Socket程式設計,覺得它是比較高深的程式設計知識,但是隻要弄清Socket程式設計的工作原理,神祕的面紗也就揭開了。
一個生活中的場景。你要打電話給一個朋友,先撥號,朋友聽到電話鈴聲後提起電話,這時你和你的朋友就建立起了連線,就可以講話了。等交流結束,結束通話電話結束此次交談。生活中的場景就解釋了這工作原理,也許TCP/IP協議族就是誕生於生活中,這也不一定。
先從伺服器端說起。伺服器端先初始化Socket,然後與埠繫結(bind),對埠進行監聽(listen),呼叫accept阻塞,等待客戶端連線。在這時如果有個客戶端初始化一個Socket,然後連線伺服器(connect),如果連線成功,這時客戶端與伺服器端的連線就建立了。客戶端傳送資料請求,伺服器端接收請求並處理請求,然後把迴應資料傳送給客戶端,客戶端讀取資料,最後關閉連線,一次互動結束。
===================================================
下面是常用的方法:
sk.bind(address)
s.bind(address) 將套接字繫結到地址。address地址的格式取決於地址族。在AF_INET下,以元組(host,port)的形式表示地址。
sk.listen(backlog)
開始監聽傳入連線。backlog指定在拒絕連線之前,可以掛起的最大連線數量。
backlog等於5,表示核心已經接到了連線請求,但伺服器還沒有呼叫accept進行處理的連線個數最大為5
這個值不能無限大,因為要在核心中維護連線佇列
sk.setblocking(bool)
是否阻塞(預設True),如果設定False,那麼accept和recv時一旦無資料,則報錯。
sk.accept()
接受連線並返回(conn,address),其中conn是新的套接字物件,可以用來接收和傳送資料。address是連線客戶端的地址。
接收TCP 客戶的連線(阻塞式)等待連線的到來
sk.connect(address)
連線到address處的套接字。一般,address的格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤。
sk.connect_ex(address)
同上,只不過會有返回值,連線成功時返回 0 ,連線失敗時候返回編碼,例如:10061
sk.close()
關閉套接字
sk.recv(bufsize[,flag])
接受套接字的資料。資料以字串形式返回,bufsize指定最多可以接收的數量。flag提供有關訊息的其他資訊,通常可以忽略。
sk.recvfrom(bufsize[.flag])
與recv()類似,但返回值是(data,address)。其中data是包含接收資料的字串,address是傳送資料的套接字地址。
sk.send(string[,flag])
將string中的資料傳送到連線的套接字。返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。即:可能未將指定內容全部發送。
sk.sendall(string[,flag])
將string中的資料傳送到連線的套接字,但在返回之前會嘗試傳送所有資料。成功返回None,失敗則丟擲異常。
內部通過遞迴呼叫send,將所有內容傳送出去。
sk.sendto(string[,flag],address)
將資料傳送到套接字,address是形式為(ipaddr,port)的元組,指定遠端地址。返回值是傳送的位元組數。該函式主要用於UDP協議。
sk.settimeout(timeout)
設定套接字操作的超時期,timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛建立套接字時設定,因為它們可能用於連線的操作(如 client 連線最多等待5s )
sk.getpeername()
返回連線套接字的遠端地址。返回值通常是元組(ipaddr,port)。
sk.getsockname()
返回套接字自己的地址。通常是一個元組(ipaddr,port)
sk.fileno()
套接字的檔案描述符
以下是例子:
例子1:
簡單的伺服器和客戶端之間進行通訊
service端:
- """
- file: service.py
- socket service
- """
- import socket
- import threading
- import time
- import sys
- def socket_service():
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 防止socket server重啟後端口被佔用(socket.error: [Errno 98] Address already in use)
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- s.bind(('127.0.0.1', 6666))
- s.listen(10)
- except socket.error as msg:
- print(msg)
- sys.exit(1)
- print('Waiting connection...')
- while 1:
- conn, addr = s.accept()
- t = threading.Thread(target=deal_data, args=(conn, addr))
- t.start()
- def deal_data(conn, addr):
- print('Accept new connection from {0}'.format(addr))
- conn.send(('Hi, Welcome to the server!').encode())
- while 1:
- data = conn.recv(1024)
- print('{0} client send data is {1}'.format(addr, data.decode()))#b'\xe8\xbf\x99\xe6\xac\xa1\xe5\x8f\xaf\xe4\xbb\xa5\xe4\xba\x86'
- time.sleep(1)
- if data == 'exit' or not data:
- print('{0} connection close'.format(addr))
- conn.send(bytes('Connection closed!'),'UTF-8')
- break
- conn.send(bytes('Hello, {0}'.format(data),"UTF-8"))#TypeError: a bytes-like object is required, not 'str'
- conn.close()
- if __name__ == '__main__':
- socket_service()
客戶端:
- import socket
- import sys
- def socket_client():
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect(('127.0.0.1', 6666))
- except socket.error as msg:
- print(msg)
- sys.exit(1)
- print(s.recv(1024))#目的在於接受:Accept new connection from (...
- while 1:
- data = input('please input work: ').encode()
- s.send(data)
- print('aa',s.recv(1024))
- if data == 'exit':
- break
- s.close()
- if __name__ == '__main__':
- socket_client()
例子二:
進行檔案的傳輸,如,.txt,.jpg等等
伺服器端:
- ###伺服器端server.py
- import socket
- import os
- import sys
- import struct
- def socket_service_image():
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- # s.bind(('127.0.0.1', 6666))
- s.bind(('127.0.0.1', 6666))
- s.listen(10)
- except socket.error as msg:
- print(msg)
- sys.exit(1)
- print("Wait for Connection.....................")
- while True:
- sock, addr = s.accept() # addr是一個元組(ip,port)
- deal_image(sock, addr)
- def deal_image(sock, addr):
- print("Accept connection from {0}".format(addr)) # 檢視傳送端的ip和埠
- while True:
- fileinfo_size = struct.calcsize('128sq')
- print('fileinfo_size is',fileinfo_size)
- buf = sock.recv(fileinfo_size) # 接收圖片名
- print('buf is ',buf)
- if buf:
- filename, filesize = struct.unpack('128sq', buf)
- print('filename ,filesize is',filename.decode(),filesize )
- fn = filename.decode().strip('\x00')
- print('fn is ',fn)
- new_filename = os.path.join('./',
- 'new_' + fn) # 在伺服器端新建圖片名(可以不用新建的,直接用原來的也行,只要客戶端和伺服器不是同一個系統或接收到的圖片和原圖片不在一個資料夾下)
- recvd_size = 0
- fp = open(new_filename, 'wb')
- while not recvd_size == filesize:
- if filesize - recvd_size > 1024:
- data = sock.recv(1024)
- recvd_size += len(data)
- else:
- data = sock.recv(1024)
- recvd_size = filesize
- print('data is',data)
- fp.write(data) # 寫入圖片資料
- fp.close()
- sock.close()
- break
- if __name__ == '__main__':
- socket_service_image()
客戶端:
- '''
- Fuction:客戶端傳送圖片和資料
- Date:
- Author:mxh
- '''
- ###客戶端client.py
- import socket
- import os
- import sys
- import struct
- def sock_client_image():
- while True:
- try:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.connect(('127.0.0.1', 6666)) # 伺服器和客戶端在不同的系統或不同的主機下時使用的ip和埠,首先要檢視伺服器所在的系統網絡卡的ip
- # s.connect(('127.0.0.1', 6666)) #伺服器和客戶端都在一個系統下時使用的ip和埠
- except socket.error as msg:
- print(msg)
- print(sys.exit(1))
- filepath = input('input the file: ') # 輸入當前目錄下的圖片名 xxx.jpg
- fhead = struct.pack(b'128sq', bytes(os.path.basename(filepath), encoding='utf-8'),
- os.stat(filepath).st_size) # 將xxx.jpg以128sq的格式打包
- s.send(fhead)
- fp = open(filepath, 'rb') # 開啟要傳輸的圖片
- while True:
- data = fp.read(1024) # 讀入圖片資料
- if not data:
- print('{0} send over...'.format(filepath))
- break
- s.send(data) # 以二進位制格式傳送圖片資料
- s.close()
- # break #迴圈傳送
- if __name__ == '__main__':
- sock_client_image()
IO多路複用
I/O(input/output),即輸入/輸出埠。每個裝置都會有一個專用的I/O地址,用來處理自己的輸入輸出資訊首先什麼是I/O:
I/O分為磁碟io和網路io,這裡說的是網路io
IO多路複用:
I/O多路複用指:通過一種機制,可以監視多個描述符(socket),一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。
Linux
Linux中的 select,poll,epoll 都是IO多路複用的機制。
Linux下網路I/O使用socket套接字來通訊,普通I/O模型只能監聽一個socket,而I/O多路複用可同時監聽多個socket.
I/O多路複用避免阻塞在io上,原本為多程序或多執行緒來接收多個連線的訊息變為單程序或單執行緒儲存多個socket的狀態後輪詢處理.
Python
Python中有一個select模組,其中提供了:select、poll、epoll三個方法,分別呼叫系統的 select,poll,epoll 從而實現IO多路複用。
對於select模組操作的方法:
控制代碼列表
11
, 控制代碼列表
22
, 控制代碼列表
33
=
select.select(控制代碼序列
1
, 控制代碼序列
2
, 控制代碼序列
3
, 超時時間)
引數: 可接受四個引數(前三個必須)
返回值:三個列表
select方法用來監視檔案控制代碼,如果控制代碼發生變化,則獲取該控制代碼。
1
、當 引數
1
序列中的控制代碼發生可讀時(accetp和read),則獲取發生變化的控制代碼並新增到 返回值
1
序列中
2
、當 引數
2
序列中含有控制代碼時,則將該序列中所有的控制代碼新增到 返回值
2
序列中
3
、當 引數
3
序列中的控制代碼發生錯誤時,則將該發生錯誤的控制代碼新增到 返回值
3
序列中
4
、當 超時時間 未設定,則select會一直阻塞,直到監聽的控制代碼發生變化
5
、當 超時時間 =
1
時,那麼如果監聽的控制代碼均無任何變化,則select會阻塞
1
秒,之後返回三個空列表,如果監聽的控制代碼有變化,則直接執行
例子1:
- 服務端:
- sk1 = socket.socket()
- sk1.bind(("127.0.0.1",8001))
- sk1.listen()
- inpu = [sk1,]
- while True:
- r_list,w_list,e_list = select.select(inpu,[],[],1)
- for sk in r_list:
- if sk == sk1:
- conn,address = sk.accept()
- inpu.append(conn)
- else:
- try:
- ret = str(sk.recv(1024),encoding="utf-8")
- sk.sendall(bytes(ret+"hao",encoding="utf-8"))
- except Exception as ex:
- inpu.remove(sk)
- 客戶端
- import socket
- obj = socket.socket()
- obj.connect(('127.0.0.1',8001))
- while True:
- inp = input("Please(q\退出):\n>>>")
- obj.sendall(bytes(inp,encoding="utf-8"))
- if inp == "q":
- break
- ret = str(obj.recv(1024),encoding="utf-8")
- print(ret)
例子2:
- 服務端:
- import socket
- sk1 = socket.socket()
- sk1.bind(("127.0.0.1",8001))
- sk1.listen()
- inputs = [sk1]
- import select
- message_dic = {}
- outputs = []
- while True:
- r_list, w_list, e_list = select.select(inputs,[],inputs,1)
- print("正在監聽的socket物件%d" % len(inputs))
- print(r_list)
- for sk1_or_conn in r_list:
- if sk1_or_conn == sk1:
- conn,address = sk1_or_conn.accept()
- inputs.append(conn)
- message_dic[conn] = []
- else:
- try:
- data_bytes = sk1_or_conn.recv(1024)
- data_str = str(data_bytes,encoding="utf-8")
- sk1_or_conn.sendall(bytes(data_str+"好",encoding="utf-8"))
- except Exception as ex:
- inputs.remove(sk1_or_conn)
- else:
- data_str = str(data_bytes,encoding="utf-8")
- message_dic[sk1_or_conn].append(data_str)
- outputs.append(sk1_or_conn)
- for conn in w_list:
- recv_str = message_dic[conn][0]
- del message_dic[conn][0]
- conn.sendall(bytes(recv_str+"好",encoding="utf-8"))
- for sk in e_list:
- inputs.remove(sk)
- 客戶端:
- import socket
- obj = socket.socket()
- obj.connect(('127.0.0.1',8001))
- while True:
- inp = input("Please(q\退出):\n>>>")
- obj.sendall(bytes(inp,encoding="utf-8"))
- if inp == "q":
- break
- ret = str(obj.recv(1024),encoding="utf-8")
- print(ret)
參考: