- 了解TCP和UDP
- 掌握编写UDP Socket客户端
- 掌握编写UDP Socket服务端应用
- 掌握编写TCP Socket客户端应用
- 掌握编写TCP Socket服务端应用
- 掌握使用socketserver模块的API编写TCP服务端应用
基本概念
IP地址与端口
IP地址
用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。如互联网的每个服务器都要有自己的IP地址,而每个局域网的计算机要通信也要配置地址。
路由器是连接两个或多个网络的设备
IP地址实际上是一个32位整数(IPV4),以字符串表示的IP如192.168.0.1实际上是把32位整数按8位分组后的数字表示,以便阅读
端口
端口是一个虚拟概念,并不是说主机上真的有若干个端口。通过端口可以在一个主机上运行多个网络应用程序,端口的表示的是一个16位的二进制整数,对应的十进制数的0~65535
网络通信协议
通过计算机网络可以实现不同计算机之间的连接与通信,但是计算机网络中实现通信必须有一些约定即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等定制标准
国际化标准组织ISO定义了网络通信协议的基本框架,称为OSI模型
OSI模型制定的七层标准,分别是:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层
但是实际上互联网通讯使用的最多的是TCP/IP网络通信协议。
TCP/IP是一个协议族,也是按照层次划分的,共四层:应用层、传输层、互连网络层、网络接口层(物理+数据链路层)
TCP/UDP
TCP协议和UDP是传输层的两种协议。Socket是传输层供给应用的编程接口,所以Socket编程就分为TCP编程和UDP编程两类
在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据,而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
这两种数据传输方式都在实际的网络编程中使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则可以通过UDP方式进行传递。
由于TCP需要建立专门的虚拟连接以及确定传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些
注:
- TCP是面向连接的,传输数据安全,稳定,效率相对较低
- UDP是面向无连接的,传输数据不稳定,效率较高
套接字编程
应用程序通常通过“套接字”(socket)向网络发出请求或者应答网络请求,使用主机之间或者一台计算机上的进程间可以通信。Python语言提供了两种网络服务的功能,其中低级别的网络服务通过套接字实现,而高级别的网络服务通过模块SocketServer实现,它提供了服务中心类,可以简化网络服务器的开发
socket()函数介绍
在python语言标准库中,通过使用socker模块提供的socket对象,可以在计算机网络中建立可以互相通信的服务器与客户端。在服务器端需要建立一个socket对象,并等待客户端的连接。客户端使用socket对象与服务器端进行连接,一旦连接成功,客户端和服务器就可以通信了
在python中,通常用一个Socket表示“打开了网络连接”,语法格式如下:
socket.socket([family[,type[,proto]]])
其中参数family:套接家族可以使用AF_UNIX或者AF_INET;type:套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM;protocol:一般不填写,默认为0
Socket主要分为面向连接的Socket和无连接的Socket。面向连接Socket使用的主要协议是传输控制协议,也就是常说的TCP,TCP的Socket名称时SOCK_STREAM。创建套接字TCP/IP套接字,可以调用socket.socket()代码如下:tcpSocket=socket.socket(AF_INET,SOCK_STREAM)
无连接Socket的主要协议是用户数据协议,也就是常说的UDP,UDP Socket的名字是SOCK_DGRAM。创建套接字UDP/IP套接字,可以调用socket.socket(),代码如下:udpSocket=socket.socket(AF_INET,SOCK_DGRAM)
UDP编程
TCP是建立可靠连接,并且通信双方都可以以流的形式发送数据。相对TCP,UDP则是面向无连接的协议。使用UDP协议时,不需要建立连接,只需要知道对方的IP地址和端口号,就可以直接发送数据包。但是,能不能到达就不知道了。虽然UDP传输数据不可靠,但是它的优点是和TCP比,速度快,对于不要求可达到的数据,就可以使用UDP协议
创建Socket时,SOCK_DGTRAM指定了这个Socket的类型就是UDP。绑定端口和TCP一样,但是不需要listen()方法,而是直接接收来自任何客户端的数据。recvfrom()方法返回数据和客户端的地址与端口,这样,服务器收到数据后,直接调用sedto()就可以把数据UDP发给客户端
from socket import socket, AF_INET,SOCK_DGRAM
#创建UDP套接字
udp_socket=socket(AF_INET,SOCK_DGRAM)
#创建接收信息的地址
addr=('192.168.217.1',8080)
data=input('请输入要发送的信息:')
#调用sendto方法发送信息
udp_socket.sendto(data.encode('gb2312'),addr)
udp_socket.close()
from socket import *
#创建UDP套接字
udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定端口
udp_socket.bind(('',8989))
addr=('192.168.111.1',8080)
data=input("请输入要发送的信息:")
#发送数据
udp_socket.sendto(data.encode('gb2312'),addr)
recv_data=udp_socket.recvfrom(1024)#表示本次接收的最大字节数1024
print('接收到%s的消息是%s'%(recv_data[1],recv_data[0].decode('gb2312')))
udp_socket.close()
UDP实现多线程聊天
from socket import *
from threading import Thread
#创建UDP套件字对象
udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定本机和端口
udp_socket.bind(('',8989))
#接收
def recv_fun():
while True:
recv_data=udp_socket.recvfrom(1024)
print('>>%s:%s'%(recv_data[1],recv_data[0].decode('gb2312')))
#发送
def send_fun():
while True:
addr=('192.168.111.1',8080)
data=input('<<:')
udp_socket.sendto(data.encode('gb2312'),addr)
if __name__ == '__main__':
#创建两个线程
t1=Thread(target=send_fun)
t2=Thread(target=recv_fun)
t1.start()
t2.start()
t1.join()
t2.join()
TFTP文件下载器
TFTP(Trivial File Transfer Protocol,简单文件传输协议)使用这个协议,就可以实现单文件的下载,tftp端口号为69
实现TFTP下载器:
下载:从服务器上将一个文本复制到本机上
下载的过程:
在本地创建一个空文件(与要下载的文件同名)
向里面写数据(接收到一点就向空文件里面写一点)
关闭(接受完所有数据关闭文件)
- TFTP文件下载过程
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端。如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面
操作码 | 功能 |
---|---|
1 | 读请求,即下载 |
2 | 写请求,即上传 |
3 | 表示数据包,即DATA |
4 | 确认码,即ACK |
5 | 错误 |
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了
struct模块可以按照指定的格式将python数据转换为字符串,该字符串为字节流。struct模块中最重要的三个函数时pack(),unpack(),calcsize()
函数名 | 描述 |
---|---|
pack(fmt,v1,v2,…) | 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流) |
unpack(fmt,string) | 按照给定的格式(fmt)解析字节流string,返回解析出来的元组 |
calcsize(fmt) | 计算给定的格式(fmt)占用了多少字节的内存 |
from socket import *
from threading import Thread
#创建UDP套件字对象
udp_socket=socket(AF_INET,SOCK_DGRAM)
#绑定本机和端口
udp_socket.bind(('',8989))
#接收
def recv_fun():
while True:
recv_data=udp_socket.recvfrom(1024)
print('>>%s:%s'%(recv_data[1],recv_data[0].decode('gb2312')))
#发送
def send_fun():
while True:
addr=('192.168.111.1',8080)
data=input('<<:')
udp_socket.sendto(data.encode('gb2312'),addr)
if __name__ == '__main__':
#创建两个线程
t1=Thread(target=send_fun)
t2=Thread(target=recv_fun)
t1.start()
t2.start()
t1.join()
t2.join()
TCP编程
面向连接的Socket使用的主要是协议传输控制协议,也就是常说的TCP的Socket的名称时SOCK_STREAM。创建套接字TCP/IP套接字,可以调用socket.socket()
tcpSocket=socket.socket(AF_INET,SOCK_STREAM)
1.第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
2.第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
3.第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进ESTABLISHED状态,完成TCP三次握手。
#导入模块
from socket import *
#创建服务器套接字对象
service_socket=socket(AF_INET,SOCK_STREAM)
#绑定端口
service_socket.bind(('',8989))
#监听
service_socket.listen()
#接收客户端的连接
client_socket,client_info=service_socket.accept()
#接收客户端发送的消息
recv_data=client_socket.recv(1024)
print('接收到%s的消息%s'%(client_info,recv_data.decode('gb2312')))
#关闭连接
client_socket.close()
service_socket.close()
from socket import *
#创建服务器端套接字对象
service_socket=socket(AF_INET,SOCK_STREAM)
#绑定端口
service_socket.bind(('',8888))
#监听
service_socket.listen()
#等待客户端的连接
client_socket,client_info=service_socket.accept()
while True:
#接收客户端的消息
recv_data=client_socket.recv(1024)
print('客户端说:',recv_data.decode('utf-8'))
if recv_data.decode('utf-8')=='bye':
break
#发送消息
msg=input('>')
client_socket.send(msg.encode('utf-8'))
client_socket.close()
service_socket.close()
from socket import *
#创建服务器端套接字对象
client_socket=socket(AF_INET,SOCK_STREAM)
client_socket.connect(('192.168.111.1',8888))
while True:
msg=input('>')
client_socket.send(msg.encode('utf-8'))
if msg=='bye':
break
#客户端接收
recv_data=client_socket.recv(1024)
print('服务器端说',recv_data.decode('utf-8'))
client_socket.close()
群聊
from socket import *
from threading import Thread
sockets=[]
def main():
# 创建server——socket套接字对象
server_socket = socket(AF_INET, SOCK_STREAM)
# 绑定端口
server_socket.bind(('', 8888))
# 监听
server_socket.listen()
#接收客户端的请求
while True:
clinet_socket,client_info=server_socket.accept()
sockets.append((clinet_socket))
#开启线程处理当前客户端的请求
t=Thread(target=readMsg,args=(clinet_socket,))
t.start()
def readMsg(client_socket):
#读取客户端发送来的消息
while True:
recv_data=client_socket.recv(1024)
#将消息发送给所有在线的客户端
if recv_data.decode('utf-8').endswith('bye'):
sockets.remove(client_socket)
client_socket.close()
break
#遍历所有在线客户端列表
if len(recv_data)>0:
for socket in sockets:
socket.send(recv_data)
if __name__ == '__main__':
main()
from socket import *
from threading import Thread
flag=True
def readMsg(client_socket):
while flag:
recv_data=client_socket.recv(1024)
print('收到:',recv_data.decode('utf-8'))
def writreMsg(client_socket):
global flag
while flag:
msg=input('>')
msg=user_name+'说:'+msg
client_socket.send(msg.encode('utf-8'))
if msg.endswith('bye'):
flag=False
break
#创建客户端的套接字对象
client_socket=socket(AF_INET,SOCK_STREAM)
#调用connect连接服务器
client_socket.connect(('192.168.111.1',8888))
user_name=input('输入名字:')
#开启一个线程处理客户端的读取消息
t1=Thread(target=readMsg,args=(client_socket,))
t1.start()
#开启一个线程处理客户端的发送消息
t2=Thread(target=writreMsg,args=(client_socket,))
t2.start()
t1.join()
t2.join()
client_socket.close()