python的網路變成比c語言簡單許多, 封裝許多底層的實現細節, 方便程式設計師使用的同時, 也使程式設計師比較難了解一些底層的東西, 我覺得學網路程式設計還是用c語言更好一點.
寫這篇博文, 也希望回顧並整理一下以前學過的c語言和linux下一些東西, 會將一些Linux網路程式設計的函式和Python網路變成函式做一個簡單的對照, 方便記憶
1. Socket套接字的概念
Socket(翻譯為套接字, 我覺得很挫),是作業系統核心中的一個資料結構,它是網路中的節點進行相互通訊的門戶。它是網路程式的ID。網路通訊,歸根到底還是程式間的通訊(不同計算機上的程式間通訊, 又稱程式間通訊, IP協議進行的主要是端到端通訊)。在網路中,每一個節點(計算機或路由)都有一個網路地址,也就是IP地址。兩個程式通訊時,首先要確定各自所在的網路節點的網路地址。但是,網路地址只能確定程式所在的計算機,而一臺計算機上很可能同時執行著多個程式,所以僅憑網路地址還不能確定到底是和網路中的哪一個程式進行通訊,因此套介面中還需要包括其他的資訊,也就是埠號(PORT)。在一臺計算機中,一個埠號一次只能分配給一個程式,也就是說,在一臺計算機中,埠號和程式之間是一一對應關係。
所以,使用埠號和網路地址的組合可以唯一的確定整個網路中的一個網路程式.
埠號的範圍從0~65535,一類是由網際網路指派名字和號碼公司ICANN負責分配給一些常用的應用程式固定使用的“周知的埠”,其值一般為0~1023, 使用者自定義埠號一般大於等於1024, 我比較喜歡用8888
每一個socket都用一個半相關描述{協議、本地地址、本地埠}來表示;一個完整的套接字則用一個相關描述{協議、本地地址、本地埠、遠端地址、遠端埠}來表示。socket也有一個類似於開啟檔案的函式呼叫,該函式返回一個整型的socket描述符,隨後的連線建立、資料傳輸等操作都是通過socket來實現的
1.1. Socket型別
socket型別在Liunx和Python是一樣的, 只是Python中的型別都定義在socket模組中, 呼叫方式socket.SOCK_XXXX
- 流式socket(SOCK_STREAM) 用於TCP通訊
流式套接字提供可靠的、面向連線的通訊流;它使用TCP協議,從而保證了資料傳輸的正確性和順序性
- 資料包socket(SOCK_DGRAM) 用於UDP通訊
資料包套接字定義了一種無連線的服務,資料通過相互獨立的報文進行傳輸,是無序的,並且不保證是可靠、無差錯的。它使用資料包協議UDP
- 原始socket(SOCK_RAW) 用於新的網路協議實現的測試等
原始套接字,普通的套接字無法處理ICMP、IGMP等網路報文,而SOCK_RAW可以, 其次,SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由使用者構造IP頭。
2. Socket程式設計
2.1. TCP通訊
TCP通訊的基本步驟如下:
服務端:socket—bind—listen—while(True){—accept—recv—send—-}—close
客戶端:socket———————————-connect—send—recv——-close
socket函式
使用給定的地址族、套接字型別、協議編號(預設為0)來建立套接字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#Linux int socket(int domain, int type, int protocol); domain:AF_INET:Ipv4網路協議 AF_INET6:IPv6網路協議 type : tcp:SOCK_STREAM udp:SOCK_DGRAM protocol : 指定socket所使用的傳輸協議編號。通常為0. 返回值:成功則返回套介面描述符,失敗返回-1。 #python socket.socket([family[, type[, proto]]]) family : AF_INET (預設ipv4), AF_INET6(ipv6) or AF_UNIX(Unix系統程式間通訊). type : SOCK_STREAM (TCP), SOCK_DGRAM(UDP) . protocol : 一般為0或者預設 如果socket建立失敗會丟擲一個socket.error異常 |
2.1.1. 伺服器端函式
bind函式
將套接字繫結到地址, python下,以元組(host,port)的形式表示地址, Linux下使用sockaddr_in結構體指標
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#Linux int bind(int sockfd, struct sockaddr * my_addr, int addrlen); sockfd : 前面socket()的返回值 my_addr : 結構體指標變數 ##### struct sockaddr_in //常用的結構體 { unsigned short int sin_family; //即為sa_family AF_INET uint16_t sin_port; //為使用的port編號 struct in_addr sin_addr; //為IP地址 unsigned char sin_zero[8]; //未使用 }; struct in_addr { uint32_t s_addr; }; #### addrlen : sockaddr的結構體長度。通常是計算sizeof(struct sockaddr); 返回值:成功則返回0,失敗返回-1 #python s.bind(address) s為socket.socket()返回的套接字物件 address為元組(host,port) host: ip地址, 為一個字串 post: 自定義主機號, 為整型 |
listen函式
使伺服器的這個埠和IP處於監聽狀態,等待網路中某一客戶機的連線請求。如果客戶端有連線請求,埠就會接受這個連線
1 2 3 4 5 6 7 8 9 10 |
#Linux int listen(int sockfd,int backlog); sockfd : 為前面socket的返回值. backlog : 指定同時能處理的最大連線要求,通常為10或者5。最大值可設至128 返回值:成功則返回0,失敗返回-1 #python s.listen(backlog) s為socket.socket()返回的套接字物件 backlog : 作業系統可以掛起的最大連線數量。該值至少為1,大部分應用程式設為5就可以了 |
accept函式
接受遠端計算機的連線請求,建立起與客戶機之間的通訊連線。伺服器處於監聽狀態時,如果某時刻獲得客戶機的連線請求,此時並不是立即處理這個請求,而是將這個請求放在等待佇列中,當系統空閒時再處理客戶機的連線請求。
1 2 3 4 5 6 7 8 9 10 11 |
#Linux int accept(int s,struct sockaddr * addr,int * addrlen); sockfd : 為前面socket的返回值. addr : 為結構體指標變數,和bind的結構體是同種型別的,系統會把遠端主機的資訊(遠端主機的地址和埠號資訊)儲存到這個指標所指的結構體中。 addrlen : 表示結構體的長度,為整型指標 返回值:成功則返回新的socket處理程式碼new_fd,失敗返回-1 #python s.accept() s為socket.socket()返回的套接字物件 返回(conn,address),其中conn是新的套接字物件,可以用來接收和傳送資料。address是連線客戶端的地址 |
2.1.2. 客戶端函式
connect函式
用來請求連線遠端伺服器
1 2 3 4 5 6 7 8 9 10 11 |
#Linux int connect (int sockfd,struct sockaddr * serv_addr,int addrlen); sockfd : 為前面socket的返回值. serv_addr : 為結構體指標變數,儲存著遠端伺服器的IP與埠號資訊 addrlen : 表示結構體變數的長度 返回值:成功則返回0,失敗返回-1 #python s.connect(address) s為socket.socket()返回的套接字物件 address : 格式為元組(hostname,port),如果連線出錯,返回socket.error錯誤 |
2.1.3. 通用函式
接收遠端主機傳來的資料
recv函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#Linux int recv(int sockfd,void *buf,int len,unsigned int flags); sockfd : 為前面accept的返回值.也就是新的套接字。 buf : 表示緩衝區 len : 表示緩衝區的長度 flags : 通常為0 返回值:成功則返回實際接收到的字元數,可能會少於你所指定的接收長度。失敗返回-1 #python s.recv(bufsize[,flag]) s為socket.socket()返回的套接字物件 bufsize : 指定要接收的資料大小 flag : 提供有關訊息的其他資訊,通常可以忽略 返回值為資料以字串形式 |
send函式
傳送資料給指定的遠端主機
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#Linux int send(int s,const void * msg,int len,unsigned int flags); sockfd : 為前面socket的返回值. msg : 一般為常量字串 len : 表示長度 flags : 通常為0 返回值:成功則返回實際傳送出去的字元數,可能會少於你所指定的傳送長度。失敗返回-1 #python s.send(string[,flag]) s為socket.socket()返回的套接字物件 string : 要傳送的字串資料 flag : 提供有關訊息的其他資訊,通常可以忽略 返回值是要傳送的位元組數量,該數量可能小於string的位元組大小。 s.sendall(string[,flag]) #完整傳送TCP資料。將string中的資料傳送到連線的套接字,但在返回之前會嘗試傳送所有資料。 返回值 : 成功返回None,失敗則丟擲異常。 |
close函式
關閉套接字
1 2 3 4 5 6 7 8 |
#Linux int close(int fd); fd : 為前面的sockfd 返回值:若檔案順利關閉則返回0,發生錯誤時返回-1 #python s.close() s為socket.socket()返回的套接字物件 |
2.2. 簡單的客戶端伺服器TCP連線
一個簡單的回顯伺服器和客戶端模型, 客戶端發出的資料, 伺服器會回顯到客戶端的終端上(只是一個簡單的模型, 沒考慮錯誤處理等問題)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#伺服器端 #!/usr/bin/env python # -*- coding:utf-8 -*- import socket #socket模組 import commands #執行系統命令模組 BUF_SIZE = 1024 #設定緩衝區大小 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #生成一個新的socket物件 server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #設定地址複用 server.bind(server_addr) #繫結地址 server.listen(5) #監聽, 最大監聽數為5 while True: client, client_addr = server.accept() #接收TCP連線, 並返回新的套接字和地址 print 'Connected by', client_addr while True : data = client.recv(BUF_SIZE) #從客戶端接收資料 print data client.sendall(data) #傳送資料到客戶端 server.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#客戶端 #!/usr/bin/env python # -*- coding:utf-8 -*- import sys import socket BUF_SIZE = 1024 #設定緩衝區的大小 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 try : client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #返回新的socket物件 except socket.error, msg : print "Creating Socket Failure. Error Code : " + str(msg[0]) + " Message : " + msg[1] sys.exit() client.connect(server_addr) #要連線的伺服器地址 while True: data = raw_input("Please input some string > ") if not data : print "input can't empty, Please input again.." continue client.sendall(data) #傳送資料到伺服器 data = client.recv(BUF_SIZE) #從伺服器端接收資料 print data client.close() |
2.2.1. 帶錯誤處理的客戶端伺服器TCP連線
在進行網路程式設計時, 最好使用大量的錯誤處理, 能夠儘量的發現錯誤, 也能夠使程式碼顯得更加嚴謹
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#伺服器端 #!/usr/bin/env python # -*- coding:utf-8 -*- import sys import socket #socket模組 BUF_SIZE = 1024 #設定緩衝區大小 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 try : server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #生成一個新的socket物件 except socket.error, msg : print "Creating Socket Failure. Error Code : " + str(msg[0]) + " Message : " + msg[1] sys.exit() print "Socket Created!" server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #設定地址複用 try : server.bind(server_addr) #繫結地址 except socket.error, msg : print "Binding Failure. Error Code : " + str(msg[0]) + " Message : " + msg[1] sys.exit() print "Socket Bind!" server.listen(5) #監聽, 最大監聽數為5 print "Socket listening" while True: client, client_addr = server.accept() #接收TCP連線, 並返回新的套接字和地址, 阻塞函式 print 'Connected by', client_addr while True : data = client.recv(BUF_SIZE) #從客戶端接收資料 print data client.sendall(data) #傳送資料到客戶端 server.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#客戶端 #!/usr/bin/env python # -*- coding:utf-8 -*- import sys import socket BUF_SIZE = 1024 #設定緩衝區的大小 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 try : client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #返回新的socket物件 except socket.error, msg : print "Creating Socket Failure. Error Code : " + str(msg[0]) + " Message : " + msg[1] sys.exit() client.connect(server_addr) #要連線的伺服器地址 while True: data = raw_input("Please input some string > ") if not data : print "input can't empty, Please input again.." continue client.sendall(data) #傳送資料到伺服器 data = client.recv(BUF_SIZE) #從伺服器端接收資料 print data client.close() |
2.3. UDP通訊
UDP通訊流程圖如下:
服務端:socket—bind—recvfrom—sendto—close
客戶端:socket———-sendto—recvfrom—close
sendto()函式
傳送UDP資料, 將資料傳送到套接字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#Linux int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen); sockfd : 為前面socket的返回值. msg : 一般為常量字串 len : 表示長度 flags : 通常為0 to : 表示目地機的IP地址和埠號資訊, 表示地址的結構體 tolen : 常常被賦值為sizeof (struct sockaddr) 返回值 : 返回實際傳送的資料位元組長度或在出現傳送錯誤時返回-1。 #Python s.sendto(string[,flag],address) s為socket.socket()返回的套接字物件 address : 指定遠端地址, 形式為(ipaddr,port)的元組 flag : 提供有關訊息的其他資訊,通常可以忽略 返回值 : 傳送的位元組數。 |
recvfrom()函式
接受UDP套接字的資料, 與recv()類似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#Linux int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen); sockfd : 為前面socket的返回值. msg : 一般為常量字串 len : 表示長度 flags : 通常為0 from :是一個struct sockaddr型別的變數,該變數儲存連線機的IP地址及埠號 fromlen : 常置為sizeof (struct sockaddr)。 返回值 : 返回接收到的位元組數或當出現錯誤時返回-1,並置相應的errno。 #Python s.recvfrom(bufsize[.flag]) 返回值 : (data,address)元組, 其中data是包含接收資料的字串,address是傳送資料的套接字地址 bufsize : 指定要接收的資料大小 flag : 提供有關訊息的其他資訊,通常可以忽略 |
2.4. 簡單的客戶端伺服器UDP連線
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#伺服器端 #!/usr/bin/env python # -*- coding:utf-8 -*- import socket BUF_SIZE = 1024 #設定緩衝區大小 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #生成新的套接字物件 server.bind(server_addr) #套接字繫結IP和埠 while True : print "waitting for data" data, client_addr = server.recvfrom(BUF_SIZE) #從客戶端接收資料 print 'Connected by', client_addr, ' Receive Data : ', data server.sendto(data, client_addr) #傳送資料給客戶端 server.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#客戶端 #!/usr/bin/env python # -*- coding:utf-8 -*- import socket import struct BUF_SIZE = 1024 #設定緩衝區 server_addr = ('127.0.0.1', 8888) #IP和埠構成表示地址 client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #生成新的套接字物件 while True : data = raw_input('Please Input data > ') client.sendto(data, server_addr) #向伺服器傳送資料 data, addr = client.recvfrom(BUF_SIZE) #從伺服器接收資料 print "Data : ", data client.close() |
2.5. 其他
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
s.getpeername() #返回連線套接字的遠端地址。返回值通常是元組(ipaddr,port)。 s.getsockname() #返回套接字自己的地址。通常是一個元組(ipaddr,port) s.setsockopt(level,optname,value) #設定給定套接字選項的值。 s.getsockopt(level,optname[.buflen]) #返回套接字選項的值。 s.settimeout(timeout) #設定套接字操作的超時期,timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛建立套接字時設定,因為它們可能用於連線的操作(如connect()) s.gettimeout() #返回當前超時期的值,單位是秒,如果沒有設定超時期,則返回None。 s.fileno() #返回套接字的檔案描述符。 s.setblocking(flag) #如果flag為0,則將套接字設為非阻塞模式,否則將套接字設為阻塞模式(預設值)。非阻塞模式下,如果呼叫recv()沒有發現任何資料,或send()呼叫無法立即傳送資料,那麼將引起socket.error異常。 s.makefile() #建立一個與該套接字相關連的檔案 |