目录

Python网络编程(1)网络编程基础

Python网络编程(1)网络编程基础

学习目标

  • IP地址和端口介绍

  • TCP的介绍

  • socket的介绍

  • TCP网络应用程序开发流程

  • TCP网络应用程序的注意点

  • TCP服务端服务于多个客户端

  • socket底层原理剖析


IP地址和端口介绍

IP地址

IP地址就是标识网络中设备的一个地址,好比现实生活中的家庭地址。

IP地址分为IPv4和IPv6地址,因为互联网中IPv4的地址资源已经枯竭,目前大部分设备都是使用IPv4,IPv6是未来使用的ip地址,IPv4是点分十进制,IPv6是冒号十六进制。

查看IP地址

  • Linux和macOS使用ifconfig这个命令。
  • Windows使用ipconfig这个命令。

检查网络是否正常

  • ping命令去测试外网网站有数据返回则表明外网通,ping命令测试当前局域网ip地址有数据返回则表明在同一个局域网内,ping命令测试127.0.0.1有数据返回表明本机的物理网卡配置正常。

端口

端口就是网络程序传输数据的通道,是数据传输的必经之路,好比教室的门。而每个教室的门都有一个门牌号,即每一个端口都会对应一个端口号,想要找到端口通过端口号即可。

什么是端口号

  • 操作系统为了统一管理端口,对端口进行编号。
  • ip地址找到设备,端口号找到通信程序的端口,然后就可以进行数据传输。
  • 端口号有65536个。
  • 一个端口号标识唯一一个端口。

端口号分类

  • 知名端口号
  • 动态端口号

知名端口号:

知名端口号是众所周知的端口号,范围从0到1023。这些端口号一般固定分配给一些服务,比如21端口号分配给FTP(文件传输协议)服务,25端口分配给SMTP(简单邮件传输协议)服务,80端口分配给HTTP服务器。

动态端口号:

一般程序员开发应用程序使用端口号称为动态端口号,范围是从1024到65535

  • 如果程序员开发的程序没有设置端口号,操作系统会在动态端口号这个范围内随机生成一个给开发的应用程序使用。
  • 当运行一个程序默认会有一个端口号,当这个程序退出时,所占用的这个端口号就会释放。

TCP的介绍(传输层)

之前学习了IP地址和端口号,通过IP地址能够找到对应的设备,然后再通过端口号找到对应的端口,再通过端口把数据传输给应用程序,数据不能随便发送,在发送之前还需要选择一个传输协议,保证程序之间按照指定的传输规则进行数据的通信,这个协议就是我们所学的TCP协议。

TCP协议:Transmission Control Protocol简称传输控制协议,它是一种面向连接,可靠的基于字节流的的传输层通信协议

面向连接意思就是在通信传输之前,先打开一个连接,连接被关闭时无法发送数据。

  1. 面向连接 2.可靠 3.基于字节流 4.传输层协议

TCP通信步骤

  • 创建连接
  • 传输数据
  • 关闭连接

TCP通信模型相当于生活中的打电话,在通信开始之前,一定要先建立好连接,才能发送数据,通信结束要关闭连接。

TCP的特点

  1. 面向连接:通信双方必须建立好连接才能进行数据的传输,数据传输完成后,双方必须要断开此连接,以释放系统资源
  2. 可靠传输:TCP采用发送应答机制(发过去的数据包有回应);超时重传;错误校验(数据包的顺序可以自己调整正确);流量控制和阻塞管理(缓冲区保留数据包)。

广播使用UDP(不可靠发包传输),聊天数据传输,文件下载,浏览器上网s使用TCP;http协议也是基于TCP协议。


socket的介绍(传输层)

socket(套接字)是进程之间通信的一个工具,好比现实生活中的插座,所有家电工作基于插座

进行,进程之间想要进行网络通信需要基于这个socket。socket是进程之间数据的搬运工。


TCP网络应用程序开发流程

简单介绍

TCP网络应用程序开发分为:1. TCP客户端程序开发,2. TCP服务端程序开发。

客户端程序指的是运行在用户设备上的程序,服务端程序指的是运行在服务器上的程序,专门为客户端提供数据服务。

../../../images/TCP原理-0599837.jpeg

客户端步骤说明

  1. 创建客户端套接字对象
  2. 和服务端套接字建立连接(三次握手,客户端先发送请求给服务端,服务端回复请求给客户端,客户端告诉服务端准备好,即建立连接)
  3. 发送数据(二进制数据)
  4. 接受数据
  5. 关闭客户端套接字

服务端步骤说明

  1. 创建服务端端套接字对象
  2. 绑定端口号
  3. 设置监听
  4. 等待接受客户端的连接请求
  5. 接受数据
  6. 发送数据
  7. 关闭套接字

开发实现

TCP客户端网络程序的开发

步骤回顾:

  1. 创建客户段套接字对象
  2. 和服务端套接字建立连接
  3. 发送数据
  4. 接受数据
  5. 关闭客户端套接字

使用socket类

导入socket模块,创建socket对象

1
2
import socket
socket.socket(AddressFamily, Type)

参数说明:

  • AddressFamily 表示的是IP地址类型,分别为IPv4和IPv6
  • Type表示传输协议类型

方法说明:

  • connect((host, port)):表示和服务端套接字建立连接,host是服务器ip地址,port是应用程序的端口号。
  • send(data):表示发送数据,data是二进制数据
  • recv(buffersize):表示接收数据,buffersize是每次接受数据的长度。

实例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import socket

if __name__ == '__main__':
    # AF_INET 表示IPv4地址类型,SOCK_STREAM 表示tcp传输协议类型
    # 建立客户端套接字对象
    tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 建立和服务端的连接
    tcp_client_socket.connect(("192.168.0.102", 9090))
    tcp_client_socket.send("你好这里是客户端".encode("UTF-8"))
    recv_data = tcp_client_socket.recv(1024)    # 1024表示一次接受的最大数据
    print("接受的数据是:"+ recv_data)
    tcp_client_socket.close()

TCP服务端网络程序的开发

步骤回顾:

  1. 创建服务端套接字对象
  2. 绑定端口号
  3. 设置监听
  4. 等待接受客户端的连接请求
  5. 接受数据
  6. 发送数据
  7. 关闭套接字

使用socket类

导入socket模块,创建socket对象

1
2
import socket
socket.socket(AddressFamily, Type)

参数说明:

  • AddressFamily 表示的是IP地址类型,分别为IPv4和IPv6
  • Type表示传输协议类型

方法说明:

  • bind((host,port)):表示绑定端口号,host是ip地址(建议不指定,防止服务器上多个网卡),port是端口号,ip地址一般不指定,表示本机的任何一个ip地址都可以。

  • listen(backlog):表示设置监听,backlog参数表示最大等待建立的个数。

  • accept():表示等待接受客户端的连接请求,如果连接成功开始使用新的套接字,保证和多个客户端通信。

  • send(data):表示发送数据,data是二进制数据

  • recv(buffersize):表示接收数据,buffersize是每次接受数据的长度。

也就是说服务端建立一个套接字是为了建立连接请求,而后分配给每个客户端一个套接字接收和发送数据。

实例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import socket
"""
1. 创建套接字
2. 绑定端口号
3. 设置监听
4. 等待连接请求
5. 接受客户端的数据
6. 发送数据到客户端
7. 关闭套接字
"""
if __name__ == '__main__':
    # 创建套接字
    server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_tcp_socket.bind(('',9091)) # 不指定ip绑定端口号
    server_tcp_socket.listen(5) # 最大等待建立连接
    new_socket,ip_addr = server_tcp_socket.accept() # 开始接受请求,程序会阻塞,返回元组(新的套接字,地址和端口号)
    print('客户端IP地址为:', ip_addr)
    new_socket.send('你收到了一条来自服务器的消息'.encode("UTF-8"))
    data = new_socket.recv(1024).decode('UTF-8')
    print(data)
    new_socket.close() # 关闭服务于客户端的收发数据的套接字
    server_tcp_socket.close()   # 关闭建立连接请求的的套接字

设置端口号复用

客户端和服务端建立请求连接成功时,当服务端程序意外关闭时,端口号不会立即释放,需要大概等1-2分钟后才能释放。解决方法就是:1. 更换服务端的端口号;2. 设置端口号复用,也就是说让服务端程序退出后端口号立即释放。

代码如下:

1
2
# 参数1表示当前套接字;参数2设置端口号复用选项;参数3设置端口号复用选项对应的值
server_tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)

设置端口号复用保证了服务端在意外关闭时端口号可以立即释放。


TCP网络应用程序的注意点

  1. 当TCP客户端程序想要和TCP服务端程序进行通信的时候必须要先建立连接
  2. TCP客户端程序一般不需要绑定端口号,因为客户端时主动发起建立连接的一方。
  3. TCP服务端必须绑定端口号,否则客户端找不到这个TCP服务端的程序。
  4. listen后的套接字时被动套接字,只负责接受新的客户端的连接请求,不能收发消息
  5. 当TCP客户端程序和TCP服务端程序连接成功后,TCP服务器端程序会产生一个新的套接字,收发客户端消息使用该套接字。
  6. 关闭accept返回的套接字意味着和这个客户端已经通信完毕
  7. 关闭listen后的套接字意味着服务端的套接字关闭了,会导致新的客户端不能连接服务端,但是之前已经成功的客户端还能正常通信
  8. 当客户端的套接字调用close后,服务端的recv会解阻塞,返回的数据长度为0,服务端可以通过返回数据的长度来判断客户端是否下线,反之服务端关闭套接字,客户端的recv也会解阻塞,返回的数据长度也为0

TCP服务端服务于多个客户端

上述的服务端程序的开发有一个弊端,就是一个服务端只能接受一个客户端的数据,因而我们初步设想将创建新接受客户端的套接字写入到循环中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if __name__ == '__main__':
    # 创建套接字
    server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) # 设置端口复用
    server_tcp_socket.bind(('',9091)) # 不指定ip绑定端口号
    server_tcp_socket.listen(128) # 最大等待建立连接
    while True:
        new_socket,ip_addr = server_tcp_socket.accept() # 开始接受请求,程序会阻塞,返回元组(新的套接字,地址和端口号)
        print('客户端IP地址为:', ip_addr)
        recv_data = new_socket.recv(1024)
        print('接收到客户端的消息:',recv_data.decode('UTF-8'),'数据长度为:',len(recv_data))
        new_socket.close() # 关闭服务于客户端的收发数据的套接字
    server_tcp_socket.close()   # 关闭等待连接请求的的套接字

虽然此代码可以实现接受多用户的数据,但是还是必须得按照顺序依次接受客户端的数据,也就是说第一个客户端发送完数据后第二个客户端才能进行链接。为更好的实现多任务连接我们在服务端用上多线程。

多任务版TCP服务端程序的开发

需求:使用多线程,因为比进程更加节省内存资源,来实现多任务。

步骤:1. 编写一个TCP服务端程序,循环等待接受客户端的连接请求,2. 当客户端和服务端建立连接成功,创建子线程,使用子线程专门处理客户端的的请求,防止主线程阻塞。3.把创建的子线程设置成为守护主线程,防止主线程无法退出。

代码

 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
import socket
import threading
# 处理客户端请求的任务
def handle_client_request(data,ip):
    print('客户端IP地址和端口为:', ip)
    # 循环接客户端的消息
    while True:
        recv_data = data.recv(1024)
        if recv_data:
            print('接收到客户端的消息:', recv_data.decode('UTF-8'), '数据长度为:', len(recv_data))
        else:
            print('客户端不发消息,下线')
            break
    new_socket.close()  # 关闭服务于客户端的收发数据的套接字

if __name__ == '__main__':
    server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) # 设置端口复用
    server_tcp_socket.bind(('',9091)) # 不指定ip绑定端口号
    server_tcp_socket.listen(128) # 最大等待建立连接
    while True:
        new_socket,ip_addr = server_tcp_socket.accept() # 主线程开始接受请求,程序会阻塞,返回元组(新的套接字,地址和端口号)
        # 客户端和服务端建立成功后这里开始子线程执行
        sub_thread = threading.Thread(target=handle_client_request,args=(new_socket,ip_addr))
        sub_thread.daemon = True  # 子线程不退出,主线程不会退出,设置守护主线程
        sub_thread.start()
    server_tcp_socket.close()   # 主线程销毁,则子线程跟着销毁

socket底层原理剖析

但创建一个TCP socket套接字对象后,会有一个发送缓冲区接受缓冲区这个发送和接受缓冲区指的就是内存中的一片空间

send原理

socket对象调用send方法时,不是直接把数据发给服务器,而是得通过网卡发送数据,应用程序是无法直接通过网卡发送数据的,它需要调用操作系统的接口,也就是说应用程序把发送的数据先写入到发送缓冲区(内存中的一片空间),再由操作系统控制网卡把发送缓冲区的数据发送给服务端网卡

recv原理

同样的socket对象调用recv方法时,应用程序也不是直接从网卡接受到的,它需要调用操作系统接口,由操作系统通过网卡接收数据,把接收的数据写入到接收缓冲区(内存中的一片空间),应用程序再从接收缓存区获取客户端发送的数据。

⚠️说明:发送数据时,应用程序将数据经由socket套接字,先保存到发送缓冲区内存,然后再经由内存将数据交给网卡,再通过网卡发给另一个设备;接收数据时,是先通过网卡把数据放在接收缓冲区,再将缓冲区里的数据经由socket套接字拿给应用程序。

小结:不管是send还是recv都不是直接将数据发送和接收到对方,而是使用socket套接字创建对应的缓冲区,接收数据使用接收缓冲区,发送数据使用发送缓冲区,缓冲区里的数据经过操作系统底层接口的调用给到网卡,进而进行数据的交互。


总结

  • IP地址是标识网络中设备的一个地址,分为IPv4和IPv6,现在多使用IPv4,未来使用IPv6,本机ip地址为127.0.0.1,域名为localhost。
  • 端口是传输数据的通道,每个程序都有一个端口,找到端口需要用到端口号,端口号用来标识端口,端口号分为知名端口号(0-1023)和动态端口(1024-65535)。
  • TCP是传输控制协议,特点是:面向连接,可靠,基于字节流,通信步骤是:第一步创建连接,第二步传输数据,第三部关闭连接。
  • socket是进程之间通信的一个工具,在进程上有一个套接字,负责进程之间数据传输。
  • TCP开发分为客户端和服务端程序,开发客户端步骤是:第一步创建套接字,第二步建立连接,第三步可以发送或者接受数据,第四步关闭套接字。开发服务端的步骤:第一步创建套接字,第二步绑定ip端口号,第三步设置监听,第四步等待连接(发生三次握手),第五步接受或者发送数据,第六步关闭服务于客户端套接字(关闭服务端套接字)。
  • 开发过程中注意设置端口复用,在开发服务端TCP程序时注意接受发送数据时可以利用多线程。
  • socket通信的流程是客户端使用套接字创建对应的缓冲区,缓冲区里的数据经过操作系统底层接口调用给网卡,相应的服务端也是。