计算机网络就是把各个计算机连接起来,让网络中的的计算机可以相互通信,而网络编程就是在程序中实现计算机相互通信。
举个例子,当你使用电脑访问微博时,你的电脑就和微博的某台服务器通过互联网连接起来了,服务器把网页上的内容作为数据通过互联网传输到你的电脑上。
更具体一点,由于你电脑上同时也存在着和其他服务器的连接,如qq服务器,所以,更确切的说,网络通信,其实时两台计算机上的进程的通信
网络编程对所有的编程语言都是一样的,就是在Python程序本身这个进程上,连接别的服务器进程的通信端口进行通信
接下来作者介绍了Python网络编程的概念和最主要的两种网络类型的编程
TCP/IP简介
互联网和计算机网络
计算机网络要比互联网出现早的多,虽然他们都是把多个计算机连在一块,但是早期的计算机网络的通信协议,每个厂商都有一个,互不兼容,而想要把计算机连在一起,就必须规定他们之间的通信协议,并且只有通信协议相同的才能进行通信,才能真正的连在一起。而互联网Internet由inter和net两个单词组成,本质上就是连接网络的网络。所以要想让所有的计算机连在一起,就必须指定一个统一的、通用的通信协议,这个协议就是互联网协议簇。
互联网协议包含上百个不同的协议,而其中最重要的就是TCP/IP协议,所以互联网协议又被称为TCP/IP协议
IP
好比现实生活中写信需要知道收信人的地址外,计算机之间通信也需要知道对方的地址,而IP就是计算机的地址。
IP地址形如127.0.0.1但它其实上是一个32位的整数(IPV4),为了方便阅读,就把他分割为4个8位一组的数组表示
还有一种IP地址就是IPV6.IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334
。
在互联网上每台计算机唯一的标识就是IP地址,如果一台计算机接入了两个 或者多个的计算机网络,比如路由器,那么他就有两个或者多个的IP地址。所以IP地址对应的就是计算机的网络接口,通常是网卡
端口
网络通信本质上是两台计算机上的两个进程之间的通信,一台计算机上存在着多个进程,如果仅知道对方的IP地址,当对方收到数据后,到底是把它给qq呢,还是给浏览器呢,这就需要端口来区别,每个网络程序都向操作系统申请一个唯一的端口号,这样,两个进程在两台计算机之间建立连接就需要知道IP地址和响应的端口号
一个进程可以与多个计算机建立连接,因此一个进程可以有多个端口号
IP协议
IP协议负责把数据通过网络从一台计算机发送到另一台计算机。数据被切分成一块块,加上其他IP协议需要用到的直接,被打包成IP包发送出去。
IP包的特点是按块发送,途径多个路由,不保证到达、不保证顺序到达
由于网络链路的复杂,两台计算机之间可能存在着多条路径,因此路由器就决定如何把一个IP包转发出去。
TCP协议
TCP协议建立在IP协议上,负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议通过握手建立连接,然后对每个IP包编号,确保对方按顺序收到,如果没有收到,就重发。它保证数据传输的可靠性
许多常用的更高级的协议都是即与TCP的,比如浏览器的HTTP协议,发送邮件的SMTP协议
一个TCP报文除了包含要传输的数据外,还包含源IP地址和目标IP地址,源端口和目标端口。
TCP编程
Socket是一个网络编程的抽象概念,表示”打开了一个网络连接”,而打开一个Socket需要知道目标计算机的IP地址和端口,然后指定相应的协议类型即可
大多数连接使用的都是可靠的TCP连接,在TCP连接中,主动发起请求的称为客户端,被动响应的称为服务端,当客户端向服务端发起连接,如果一切顺利,服务端接受了请求,一个TCP连接就建立起来了
客户端
在客户端建立连接需要五步
第一步:创建一个基于TCP协议的Socket
# 导入socket库:
import socket
# 创建一个socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)#AF_INET
指定使用IPv4协议,AF_INET6指定IPV6,SOCK_STREAM
指定使用面向流的TCP协议
提供网页服务的服务器必须把端口号固定在80端口,因为80端口是web服务的标准端口,其他服务都有对应的标准端口号,例如SMTP服务是25
端口,FTP服务是21
端口,等等。端口号小于1024的是Internet标准服务的端口,端口号大于1024的,可以任意使用。
第二步:连接服务器
s.connect(('www.sina.com.cn', 80))#注意中间的括号
第三步:建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:
# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')
TCP连接创建的是双向通道,双方都可以同时给对方发送数据,但是谁先发谁后发,怎么协调,会根据具体的协议指定,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。
发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:
第四步:接受数据
# 接收数据:
buffer = []
while True:
# 每次最多接收1k字节:
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
第五步:当我们接收完数据后,调用close()
方法关闭Socket,这样,一次完整的网络通信就结束了:
# 关闭连接:
s.close()
接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:
header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
f.write(html)
现在,只需要在浏览器中打开这个sina.html
文件,就可以看到新浪的首页了。
服务器
服务器编程相对复杂一点
需要绑定端口用来监听连接请求,需要位每个请求新开一个线程来处理
服务器进程首先要绑定一个端口并监听来自其他客户端的连接。如果某个客户端连接过来了,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接了。
所以,服务器会打开固定端口(比如80)监听,每来一个客户端连接,就创建该Socket连接。由于服务器会有大量来自客户端的连接,所以,服务器要能够区分一个Socket连接是和哪个客户端绑定的。一个Socket依赖4项:服务器地址、服务器端口、客户端地址、客户端端口来唯一确定一个Socket。
但是服务器还需要同时响应多个客户端的请求,所以,每个连接都需要一个新的进程或者新的线程来处理,否则,服务器一次就只能服务一个客户端了。
我们来编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello
再发回去。
import socket,threading,time
def main():
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#首先,创建一个基于IPv4和TCP协议的Socket:
s.bind(('127.0.0.1',9999))#端口号需要预先指定。
s.listen(5)#调用listen()
方法开始监听端口,传入的参数指定等待连接的最大数量
print('Waitting for connection...')
while True:
sock,addr=s.accept()#accept()
会等待并返回一个客户端的连接:
t=threading.Thread(target=tcplink,args=(sock,addr))
t.start()
def tcplink(sock,adds):
print('Accept new connection form %s:%s'%adds)
sock.send(b'Welcome')
while True:
data=sock.recv(1024)#之后所有的操作都由这个socket
time.sleep(1)
if not data or data.decode('utf-8')=='exit':
break
sock.send(('hello%s'%data.decode('utf-8')).encode('utf-8'))#这里send不加b''
sock.close()
print('connection from %s:%s closed'%addr)
if __name__ == "__main__":
main()
我们要绑定监听的地址和端口。服务器可能有多块网卡,可以绑定到某一块网卡的IP地址上,也可以用0.0.0.0
绑定到所有的网络地址,还可以用127.0.0.1
绑定到本机地址。127.0.0.1
是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,也就是说,外部的计算机无法连接进来。
因为我们写的这个服务不是标准服务,所以用9999
这个端口号。请注意,小于1024
的端口号必须要有管理员权限才能绑定
服务器程序通过一个永久循环来接受来自客户端的连接
每个连接都必须创建新线程(或进程)来处理,否则,单线程在处理连接的过程中,无法接受其他客户端的连接
要测试这个服务器程序,我们还需要编写一个客户端程序:
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',9999))
# print(s.recv(1024).decode('utf-8'))#如果这里不接受会怎么样
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.send(data)
print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()
用TCP协议进行Socket编程在Python中十分简单,对于客户端,要主动连接服务器的IP和指定端口,对于服务器,要首先监听指定端口,然后,对每一个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去。
同一个端口,被一个Socket绑定了以后,就不能被别的Socket绑定了。
UDP编程
使用UDP协议时,不需要建立连接,知道目标计算机的IP地址和端口后,可以直接向目标计算机发送数据。它的优点时速度快,缺点时不能保证传输的可靠性
使用UDP协议也需要区分客户端和服务端。
客户端
新建一个基于UDP协议的Socket对象,不需要与服务器建立连接,向服务器发送数据,接受服务器的数据,虽然不需要建立连接,但是需要关闭打开的Socket
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# s.connect(('127.0.0.1',9999))
# print(s.recv(1024).decode('utf-8'))
for data in [b'Michael', b'Tracy', b'Sarah']:
# 发送数据:
s.sendto(data,('127.0.0.1',9999))
print(s.recv(1024).decode('utf-8'))
s.close()
服务端
新建一个Socket对象、然后绑定IP地址和端口号。不需要LIsten()方法,可以直接接受数据。可以调用resvfrom(size)返回最多size大小的数据、和对方的地址。可以直接调用sentto()方法向对方发送数据
多线程省去了
import socket
def main():
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind(('127.0.0.1',9999))
while True:
data,addr=s.recvfrom(1024)
print('Recived from address%s:%s'%addr)
s.sendto('Hello %s'%data,addr)
此外,UDP的端口号和TCP的端口号不冲突,可以同时将UDP和TCP的端口申请为9999