(2021.07.04 Sun)
socket基本
socket(套接字)是计算机网络节点间和进程间通信的一种方式/约定,网络节点可通过socket发送或接收信息。socket封装和提供了网络协议(栈)的接口,通过接口可控制协议栈的工作,实现跨主机网络通信。
在同一台主机中,进程可以通过唯一进程标识符识别,即PID,而跨主机间的进程用PID不适合。在socket中,不同主机的进程通过网络地址、协议和端口的三元组来唯一标识进程,即(IP, protocol, port)。
Unix/Linux的基本哲学是"一切皆文件",文件可以通过"打开"、"读写"、"关闭"的方式进行操作。socket继承了这种哲学。在socket中,网络上的设备都被当做文件,并赋值文件标识符,对这些设备的操作,可类比于对本地磁盘上的文件进行操作。
socket中封装了网络协议的实现。一个典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
Python、JAVA等都提供了对socket的支持,并提供了专用包。
socket类型
SOCK_STREAM流格式套接字
SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。该格式使用了TCP作为传输层协议,因此提供了可靠通信,即数据的无损和正序。
流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。
HTTP使用了SOCK_STREAM套接字。
SOCK_DGRAM数据包格式套接字
无连接套接字,其传输层使用了UDP协议。数据传输过程中只管传输数据,不做数据校验,是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。传输时限制数据大小,发送和接收同步。常用于视频会议等场合。
(2021.08.08 Sun)
服务端和客户端创建socket连接的流程-TCP
- 服务端调用
socket
函数创建一个socket,该函数的返回值是socket唯一描述字(socket descriptor) - 服务端调用
bind
函数将地址族中的地址赋给socket,也就是将服务端的地址绑定到socket,客户端可通过该地址与服务端通信 - 服务端调用
listen
函数监听socket,判断是否有来自客户端的connect申请 - 客户端调用
connect
函数,向服务器发出连接请求 - 服务端listen到客户端的connect请求,调用
accept
函数,通过三次握手建立连接;连接建立完成,可类比文件读写操作,对socket进行操作 - 两端通过
read
/write
,或recvmsg
/sendmsg
函数对socket进行读写操作 - 操作完成调用
close
函数完成TCP连接
注: 在服务端调用accept
时,成功连接后会返回一个已完成连接的socket,用来传输数据。监听的socket和用于数据传输的socket是两个socket。
socket中三次握手的流程
当客户端调用connect
时,触发了连接请求,向服务器发送了SYN J包,这时connect
进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept
函数接收请求向客户端发送SYN K ,ACK J+1,这时accept
进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect
返回,并对SYN K进行确认;服务器收到ACK K+1时,accept
返回,至此三次握手完毕,连接建立。
socket demo, in Python
# 2021.08.08 Sun
# socket demo: tcp
import argparse, socket
def recvall(sock, length):
data = b''
while len(data) < length:
more = sock.recv(length - len(data))
print(more,'\n')
if not more:
# raise EOFError('was expecting %d bytes but only received %d bytes before the socekt closed' % (length, len(data)))
raise EOFError('was expecting {} bytes but only received {} bytes before the socekt closed'.format(length, len(data)))
data += more
return data
def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((interface, port))
sock.listen(1)
print('listening at ', sock.getsockname())
while True:
sc, sockname = sock.accept()
print('we have accepted a connection from ', sockname)
print('Socket name: ', sc.getsockname())
print('Socket peer: ', sc.getpeername())
# message = recvall(sc, 16)
message = sc.recv(8096)
print('Incoming sixteen-octet message: ', repr(message))
sc.sendall(b'ffff, client')
sc.close()
print('Reply sent, socket closed')
def client(host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))
print('client has been assigned socket name ', sock.getsockname())
sock.sendall(b'hi server')
# reply = recvall(sock, 8096)
reply = sock.recv(8096)
print('server said ', repr(reply))
sock.close()
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send and receive over TCP')
parser.add_argument('role', choices=choices, help='which role to play')
parser.add_argument('host', help='interface the server listens at; '
' host the client sends to')
parser.add_argument('-p', metavar='PORT', type=int, default=1060,
help='TCP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
print('host: ', args.host,', port: ', args.p)
function(args.host, args.p)
在服务端和客户端可同时使用该脚本。服务端调用方法:
$ python3 socket_tcp.py server '192.168.1.3' # specify host ip
客户端调用方法:
$ python3 socket_tcp.py client '192.168.1.3' # specify server host ip
设置传输字节的长度可运行该程序。
(2021.08.12 Thur)
服务端和客户端创建socket连接的流程-UDP
与TCP相比,UDP没有握手过程,在实现上的服务器端少了accept
步骤。
# socket_udp.py
# UDP client and server
import argparse, socket
from datetime import datetime
MAX_BYTES = 65535
def server(port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', port))
print('listening at {}'.format(socket.getsockname()))
while True:
data, address = sock.recvfrom(MAX_BYTES)
text = data.decode('ascii')
print('client at {} says {!r}'.format(address, text))
text = 'your data was {} bytes long'.format(len(data))
data = text.encode('ascii')
sock.sendto(data, address)
def client(port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
text = 'The time is {}'.format(datatime.now())
data = text.encode('ascii')
sock.sendto(data, ('127.0.0.1', port))
print('The OS assigned me the address {}'.format(sock.getsockname()))
data, address = sock.recvfrom(MAX_BYTES)
text = data.decode('ascii')
print('the server {} replied {!r}'.format(address, text))
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send adn receive UDP locally')
parser.add_argument('role', choices=choices, help='which role to play')
parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='UDP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
function(args.p)
在服务器端的输入
$python3 socket_udp.py server
Listening at ('127.0.0.1', 1060)
在客户端的输入
$python3 socket_udp.py client
注意到与TCP不同的是,代码中并没有套接字的connect()
函数。如果使用sento()
函数,那么每次向服务器发送信息的时候都必须显式的给出服务器的IP地址和端口;而如果使用connec()
调用,操作系统事先就已经知道数据包要发送到的远程地址,这样就可以简单的把要发送的数据作为参数传入send()
调用,无需重复给出服务器地址。
广播和多播(multicast)
UDP有个特别强大的功能:广播。通过广播,数据包的目标地址被设置为本机连接的整个子网,然后使用物理网卡将数据包广播,就无需复制该数据包并单独将其发送给所有连接到该子网的主机。这里暂只介绍广播。
广播发生在接收广播数据包的服务器和发送广播数据包的客户端之间。和前面UDP的例子不同之处在于,使用socket对象时,先调用setsockopt()
方法,设置为允许进行广播。
# socket upd broadcast
import argparse, socket
BUFSIZE = 65535
def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((interface, port))
print('Listening for dataframs at {}'.format(sock.getsockname()))
while True:
data, address = sock.recvfrom(BUFSIZE)
text = data.decode('ascii')
print('the client at {} say: {!r}'.format(address, text))
def client(network, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
text = 'Broadcast datagram.'
sock.sendto(text.encode('ascii'), (network, port))
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send, receive UDP broadcast')
parser.add_argument('role', choices=choices, help='which role to take')
parser.add_argument('host', help='interface the server listens at; network the client sends to')
parser.add_argument('-p', metavar='port', type=int, default=1060, help='UDP port (default 1060)')
args = parser.parse_args()
function = choices[args.role]
function(args.host, args.p)
运行方法
首先使用下面指令运行一到两台网络中的服务器
$python socket_udp_broadcast.py server ""
Listing for broadcasts at ('0.0.0.0', 1060)
保持各服务器运行。在客户端上输入ifconfig
找到broadcast地址,比如192.168.1.255
,在本地网络上运行
$python socket_udp_broadcast.py client 192.168.1.255
另一种广播的方法
,不需要在本地获得广播地址,并且向本机的所有网络端口进行广播。
$python socket_udp_broadcast.py client ""
(2021.08.15 Sun)
Socket参数
在socket初始化过程中,需要对socket做如下设置
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((host, port))
除了指定了服务器/客户端的IP和port,还需要指定socket方法中的两个参数,即地址族(address family)和套接字类型(socket type)。另有一个隐藏参数时协议(protocol),暂不讨论。
地址族
某个特定的机器可能连接到多个不同类型的网络。对地址族的选择指定了想要进行通信的网络类型。可找出socket的方法并打印以AF_
(address family)开头的符号,查看其它选择,诸如AF_APPLETALK
,AF_BLUETOOTH
等。TCP和UDP可用AF_INET
。
套接字类型
该类型给出了希望在已经选择的网络上使用的特定通信技术。
TCP选SOCK_STREAM,UDP选SOCK_DGRAM。
地址解析
socket.getaddrinfo()
用于地址解析。
Reference
1 图片源自网络
2 Brandon R. and etc,诸豪文译,Python网络编程(第3版),中国工信出版社,人民邮电出版社