网络编程,是指让在不同的电脑上的软件能够进行数据传递,即进程之间的通信。

本地的进程间通信(IPC)

例如有:队列、同步(互斥锁、条件变量等)等,这些通信方式都是一台机器上不同进程之间的通信方式。

网络中的进程间通信

首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!

在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。

其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。

这样利用 IP 地址、协议、端口 就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

Socket

Socket(简称套接字 )是一个位于网络中特定节点的服务所具有的标识符,它包含了一个节点地址和一个端口号,用来标识这一服务。

套接字是 进程间通信的一种方式 ,它与其他进程间通信的一个主要不同是: 它能实现 不同主机间的进程间通信 ,我们网络上各种各样的服务大多都是基于 Socket 来完成通信的。 例如我们每天浏览网页、QQ 聊天、收发 email 等等。

创建 socket

在 python 中使用 socket 模块的函数 socket :

socket.socket(AddressFamily, Type)

函数 socket.socket 创建一个 socket,返回该 socket 的描述符,该函数带有两个参数:

  • Address Family:可以选择 AF_INET(用于 Internet 进程间通信) 或者 AF_UNIX(用于同一台机器进程间通信),实际工作中常用 AF_INET。
  • Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)。
  • 1 >>> import socket
    2 >>> # 创建一个 TCP Socket
    3 >>> tcp_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    4 >>> tcp_s  
    5 <socket.socket fd=548, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
    6 >>> # 创建一个 UDP Socket
    7 >>> udp_s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    8 >>> udp_s  
    9 <socket.socket fd=620, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0>

    2. UDP

    2.1 UDP 介绍

    UDP(User Datagram Protocol)——用户数据报协议,是一个无连接的、简单的、面向数据报的传输层协议。

    不具有可靠性的数据报协议。它只是把应用程序传给 IP 层的数据报发送出去,但是并不能保证它们能到达目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
  • 由于 UDP 在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,包数据顺序乱掉时也没有纠正的功能,再加上 UDP 本身的处理既简单又高效,故而传输速度很快。
  • 将数据封装为数据包,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,每个数据包的大小限制在 64K 中。

    应用场景:注重速度流畅

    UDP 是面向消息的协议,通信时不需要建立连接,数据的传输自然是不可靠的,UDP 一般用于多点通信和实时的数据业务,比如:

    音视频等多媒体通信(即时通信,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响)
  • 广播通信(广播、多播)
  • TFTP(简单文件传送)
  • SNMP(简单网络管理协议)
  • RIP(路由信息协议,如报告股票市场、航空信息)
  • DNS(域名解释)
  • UDP 操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中 client/server 应用程序。例如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用 UDP 会更合理一些。

    网络通信过程

    2.2 UDP基础示例

    发送、接收数据

    创建一个 UDP 客户端程序的流程较为简单,具体步骤如下:

  • 创建客户端套接字
  • 发送/接收数据
  • 关闭套接字
  • 示例:UDP 发送方

     1 import socket
     3 # 1. 创建套接字
     4 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     6 # 2. 准备接收方的地址
     7 sendAddr = ("192.168.3.4", 8080)
     9 # 3. 从键盘获取要发送的数据
    10 sendData = input("请输入要发送的数据:")
    12 # 4. 发送数据到指定的电脑上
    13 udpSocket.send(sendData.encode(), sendAddr)
    14 # encode():将 str 转为 bytes。
    15 # decode():将 bytes 转为 str。如我们从网络或磁盘上读取了字节流,那么读到的数据就是 bytes。
    17 # 5. 等待接收方发送过来的数据
    18 recvData = udpSocket.recv(1024)  # 1024表示本次接收数据的最大字节数
    20 # 6. 显示对方发送的数据
    21 print(recvData)
    23 # 7. 关闭套接字
    24 udpSocket.close()

    执行效果:

    会变的端口号:

    重新运行多次脚本,然后在“网络调试助手”中,看到的现象如下:

  • 每重新运行一次网络程序,发送方的端口号会变化,不一样的原因在于,这个数字标识这个网络程序,当重新运行时,如果没有确定到底用哪个,系统默认会随机分配。
  • 这个网络程序在运行的过程中,这个IP+端口号就唯一标识了这个程序,所以如果其他电脑上的网络程序如果想要向此程序发送数据,那么就需要向这个数字(即端口)标识的程序发送即可。
  • 2.3 UDP 绑定信息

    一般情况下,在一台电脑上运行的网络程序会有很多,而各自用的端口号很多情况下也不知道。为了不与其他的网络程序占用同一个端口号,往往在编程中,UDP 的端口号一般不绑定。

    但是如果需要做成一个接收方的程序的话,是需要绑定的。正如如果报警电话每天都在变,想必世界就会乱了。所以一般服务性的程序,往往需要一个固定的端口号,这就是所谓的端口绑定。

     1 import socket
     3 # 创建套接字
     4 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     6 # 绑定本地的相关信息,如果一个网络程序不绑定,系统就会随机分配
     7 binAddr = ("", 7788)  # IP地址和端口号,IP一般不用写,表示本机的任何一个IP
     8 udpSocket.bind(binAddr)
    10 # 接收方的地址信息
    11 sendAddr = ("192.168.234.1", 8080)
    13 # 发送数据
    14 sendData = udpSocket.sendto("haha".encode(), sendAddr)
    16 # 等待接收方发送过来的数据
    17 recvData = udpSocket.recvfrom(1024)  # 1024表示本次接收数据的最大字节数
    19 # 显示对方发送的数据
    20 print(recvData)
    22 # 关闭套接字
    23 udpSocket.close()

    执行效果:

    2.4 UDP 小应用

    应用 1:echo 服务器,即把接收的数据再发送回去

     1 import socket
     2 import time
     5 # 创建套接字
     6 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     8 # 绑定本地的相关信息,如果一个网络程序不绑定,系统就会随机分配
     9 binAddr = ("", 7788)  # IP地址和端口号,IP一般不用写,表示本机的任何一个IP
    10 udpSocket.bind(binAddr)
    12 # 接收方的地址信息
    13 sendAddr = ("192.168.234.1", 8080)
    15 num = 1
    17 def repeat():
    18     # 等待对方发送来的数据
    19     recvData = udpSocket.recvfrom(1024)  # 1024即本次接收数据的最大字节数
    20     # 将接收的数据再次发送回去
    21     udpSocket.sendto(recvData[0], sendAddr)
    22     # 统计信息
    23     print("已经将接收到的第%d个信息返回给对方,数据内容为:%s" % (num, recvData[0].decode()))    
    25 # 若不使用 try..except... 重试机制,而会出现“远程主机强制关闭连接”的报错
    26 while True:
    27     try:
    28         repeat()
    29     except:
    30         repeat()
    32 # 关闭套接字
    33 udpSocket.close()

    注意,若出现“ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接”:出现这种原因代表远程过于频繁,所以远程怀疑是恶意攻击。

    可以使用 try...except...重试,在报错时重新调用该方法使其重新抓取,直至抓取成功。

    执行效果:

    应用 2:聊天室

    模拟一个聊天室,显示所有接收到的数据。

     1 import socket
     2 import time
     5 # 创建套接字
     6 udpSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     8 # 绑定本地的相关信息,如果一个网络程序不绑定,系统就会随机分配
     9 binAddr = ("", 7788)  # IP地址和端口号,IP一般不用写,表示本机的任何一个IP
    10 udpSocket.bind(binAddr)
    12 # 接收方的地址信息
    13 sendAddr = ("192.168.234.1", 8080)
    16 def repeat():
    17     # 等待对方发送来的数据
    18     recvData = udpSocket.recvfrom(1024)  # 1024即本次接收数据的最大字节数
    19     # 打印消息记录
    20     print("【%s】%s:%s"%(time.ctime(), recvData[1][0], recvData[0].decode()))   
    23 while True:
    24     try:
    25         repeat()
    26     except:
    27         repeat()
    30 # 关闭套接字
    31 udpSocket.close()

    执行效果: