目录
Socket
套接字
源 IP 地址和目的 IP 地址以及源端口号和目的端口号的组合称为套接字,是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,解决网络上两台主机之间的进程通信问题。简单的说就是通信双方的一种约定,用套接字中的相关函数来完成通信过程。其用于标识客户端请求的服务器和服务,是网络通信过程中端点的抽象表示,包含进行网络通信必需的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远地主机的 IP 地址、远地进程的协议端口(Socket = IP address + TCP/UDP + port)。应用层(HTTP)和传输层(TCP/UDP)就可以通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
套接字的原理
Socket 可以看成在两个程序进行通讯连接中的一个端点,是连接应用程序和网络驱动程序的桥梁,Socket 在应用程序中创建,通过绑定与网络驱动建立关系。此后,应用程序送给 Socket 的数据,由 Socket 交给网络驱动程序向网络上发送出去。计算机从网络上收到与该 Socket 绑定 IP 地址和端口号相关的数据后,由网络驱动程序交给 Socket,应用程序便可从该 Socket 中提取接收到的数据,网络应用程序就是这样通过 Socket 进行数据的发送与接收的。要通过 Internet 进行通信,至少需要一对套接字,其中一个运行在客户端,称之为 ClientSocket,另一个运行于服务器端面,称为 ServerSocket。根据连接启动的方式以及本地要连接的目标,套接字之间的连接过程可以分为三个步骤:
服务器监听:是指服务端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
客户端请求:是由客户端的套接字提出连接请求,要连接的目标是服务器端套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器套接字的地址和端口号,然后再向服务器端套接字提出连接请求。
连接确认:连接确认是当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的信息发送给客户端,一旦客户端确认了此连接,连接即可建立。而服务器端继续处于监听状态,继续接收其他客户端的连接请求。
套接字的数据处理方式
同步模式:同步模式的特点是在通过 Socket 进行连接、接收、发送数据时,客户机和服务器在接收到对方响应前会处于阻塞状态,即一直等到收到对方请求才继续执行下面的语句。可见,同步模式只适用于数据处理不太多的场合。当程序执行的任务很多时,长时间的等待可能会让用户无法忍受,但同步模式的好处在于应用接口的访问有着更好的稳定性。
异步模式:异步模式的特点是在通过 Socket 进行连接、接收、发送操作时,客户机或服务器不会处于阻塞方式,而是利用 callback 机制进行连接、接收、发送处理,这样就可以在调用发送或接收的方法后直接返回,并继续执行下面的程序。可见,异步套接字特别适用于进行大量数据处理的场合,使用
s.setblocking(0)
启用异步模式。
套接字类型
套接字类型 | 描述 |
---|---|
socket.AF_UNIX | 只能够用于单一的 Unix 系统进程间通信,同一台机器进程间通信 |
socket.AF_INET | 服务器之间网络通信,Internet 进程间通信 |
socket.AF_INET6 | IPv6 |
socket.SOCK_STREAM | 流式 socket, for TCP,流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复发送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即 TCP(The Transmission Control Protocol) 协议。 |
socket.SOCK_DGRAM | 数据报式 socket, for UDP,数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用 UDP(User Datagram Protocol) 协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理。 |
socket.SOCK_RAW | 原始套接字,普通的套接字无法处理 ICMP、IGMP 等网络报文,而 SOCK_RAW 可以。其次,SOCK_RAW 也可以处理特殊的 IPv4 报文。此外,利用原始套接字,可以通过 IP_HDRINCL 套接字选项由用户构造 IP 头。原始套接字(SOCKET_RAW)允许对较低层次的协议直接访问,比如 IP、 ICMP 协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为 RAW SOCKET 可以自如地控制 Windows 下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过 RAW SOCKET 来接收发向本机的 ICMP、IGMP 协议包,或者接收 TCP/IP 栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的 IP 包。网络监听技术很大程度上依赖于 SOCKET_RAW |
socket.SOCK_SEQPACKET | 可靠的连续数据包服务 |
创建TCP Socket: | s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) |
创建UDP Socket: | s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM) |
UDP 和 TCP 的区别:
tcp是可靠的、面向连接的、尽力传输的协议,而udp是不可靠 的、面向非连接的、不尽力传输的协议。但是不可靠不代表它没有用,udp有自己的应用场景,语音和视频几乎都在使用udp协议,它的不可靠只是相对于 tcp来说的,但是它的好处就是效率,高效在某些场景要比可靠性重要。这就涉及trade-off了,也就是权衡,需要根据你的应用权衡利弊,然后进行选择。
NOTE:
1)TCP发送数据时,已建立好TCP连接,所以不需要指定地址。UDP是面向无连接的,每次发送要指定是发给谁。
2)服务端与客户端不能直接发送列表,元组,字典。需要字符串化 repr(data)。
Socket 标准函数
ServerSocket 标准函数
Socket 函数 | 描述 |
---|---|
s.bind(address) | 将套接字绑定到地址, 在AF_INET下,以元组(host,port)的形式表示地址. |
s.listen(backlog) | 开始监听TCP传入连接。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求。 |
s.accept() | 接受TCP连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。socket 进入“waiting”状态。客户请求连接时,方法建立连接并返回服务器。accept方法返回一个含有两个元素的元组(connection,address)。第一个元素connection是新的socket对象,服务器必须通过它与客户通信;第二个元素 address是客户的Internet地址。 |
ClientSocket 标准函数
Socket 函数 | 描述 |
---|---|
s.connect(address) | 连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。 |
s.connect_ex(adddress) | 功能与connect(address)相同,但是成功返回0,失败返回errno的值。 |
公有标准函数
Socket 函数 | 描述 |
---|---|
s.recv(bufsize[,flag]) | 接受 TCP 套接字的数据。数据以字符串形式返回,bufsize 指定要接收的最大数据量。flag 提供有关消息的其他信息,通常可以忽略。recv 方法在接收数据时会进入「blocked」状态,如果发送的数据量超过了 recv 所允许的,数据会被截短。多余的数据将缓冲于接收端。以后调用 recv 时,多余的数据会从缓冲区删除(以及自上次调用 recv 以来,客户可能发送的其它任何数据)。 |
s.send(string[,flag]) | 发送 TCP 数据。将 String 中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于 String 的字节大小。 |
s.sendall(string[,flag]) | 完整发送 TCP 数据。将 String 中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回 None,失败则抛出异常。 |
s.recvfrom(bufsize[,flag]) | 接受 UDP 套接字的数据。与 recv() 类似,但返回值是(data, address)。其中 data 是包含接收数据的字符串,address 是发送数据的套接字地址。 |
s.sendto(string[,flag],address) | 发送 UDP 数据。将数据发送到套接字,address 是形式为 (ipaddr, port) 的元组,指定远程地址。返回值是发送的字节数。 |
s.close() | 关闭套接字。 |
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() | 创建一个与该套接字相关连的文件 |
Socket 编程
编程思路
TCP 服务端:
- 创建套接字,绑定套接字到本地 IP 与端口
- 开始监听连接
- 进入循环,不断接受客户端的连接请求
- 然后接收传来的数据,并发送给对方数据
- 传输完毕后,关闭套接字
TCP 客户端:
- 创建套接字,连接远端地址
- 连接后发送数据和接收数据
- 传输完毕后,关闭套接字
Demo
TCP 服务端
Step 1:创建套接字,如果创建 socket 函数失败,会抛出一个 socket.error 的异常,需要捕获。
try:
# create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
sys.exit()
Step 2:绑定 socket
HOST = '' # Symbolic name meaning all available interfaces
PORT = 8888 # Arbitrary non-privileged port
ADDR = (HOST, RORT)
try:
s.bind(ADDR)
except socket.error , msg:
print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
sys.exit()
Step 3: 监听连接
s.listen(5)
Step 4:接收请求
# wait to accept a connection - blocking call
conn, addr = s.accept()
Step 5:发送响应
# now keep talking with the client
data = conn.recv(1024)
conn.sendall(data)
Step 6:关闭 socket
s.close()
NOTE1:将上面的服务器程序改造成一直运行,最简单的办法是将 accept 放到一个循环中,那么就可以一直接收连接了。
# now keep talking with the client
while True:
#wait to accept a connection - blocking call
conn, addr = s.accept()
print 'Connected with ' + addr[0] + ':' + str(addr[1])
data = conn.recv(1024)
reply = 'OK...' + data
if not data:
break
conn.sendall(reply)
conn.close()
NOTE2:多个 Client 可以随时建立连接,而且每个客户端可以跟服务器进行多次通信,将处理的程序与主程序的接收连接分开。一种方法可以使用线程来实现,主服务程序接收连接,创建一个线程来处理该连接的通信,然后服务器回到接收其他连接的逻辑上来。
- 定义线程体函数
# Function for handling connections. This will be used to create threads
def client_thread(conn):
# Sending message to connected client
conn.send('Welcome to the server. Type something and hit enter\n') # send only takes string
# infinite loop so that function do not terminate and thread do not end.
while True:
# Receiving from client
data = conn.recv(1024)
reply = 'OK...' + data
if not data:
break
conn.sendall(reply)
# came out of loop
conn.close()
- 创建多线程
# now keep talking with the client
while True:
# wait to accept a connection - blocking call
conn, addr = s.accept()
print 'Connected with ' + addr[0] + ':' + str(addr[1])
# start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function.
start_new_thread(client_thread ,(conn,))
s.close()
TCP 客户端
Step 1:创建套接字,如果创建 socket 函数失败,会抛出一个 socket.error 的异常,需要捕获。
try:
# create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
sys.exit()
Step 2: 连接
remote_hostname = 'www.google.com'
try:
# 获得远程主机的 IP 地址
remote_ip = socket.gethostbyname(remote_hostname)
except socket.gaierror:
#could not resolve
print 'Hostname could not be resolved. Exiting'
sys.exit()
s.connect((remote_ip , port))
Step 3:发送请求
try :
#Set the whole string
s.sendall(message)
except socket.error:
#Send failed
print 'Send failed'
sys.exit()
Step 4:接收响应
reply = s.recv(4096)
Step 5:关闭 socket
s.close()