Python3学习笔记_I(网络编程及其应用)


笔记中代码均可运行在Jupyter NoteBook下(实际上Jupyter-lab使用体验也很棒)。

建议不要光看,要多动手敲代码。眼过千遭,不如手读一遍。

相关笔记的jupiter运行代码已经上传,请在资源中自行下载。

网络编程

使用网络能够把多方链接在一起,然后可以进行数据传递

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

Python Internet 模块

协议 功能用处 端口号 Python模块
HTTP 网页访问 80 httplib, urllib, xmlrpclib
NNTP 阅读和张贴新闻文章,俗称为"帖子" 119 nntplib
FTP 文件传输 20 fitlib, urllib
SMTP 发送邮件 25 smtplib
POP3 接收邮件 110 poplib
IMAP4 获取邮件 143 imaplib
Telnet 命令行 23 telnetlib
Gopher 信息查找 70 gopherlib,urllib

TCP/IP协议(族)

应用层:应用层,表示层,会话层

传输层:传输层

网络层:网络层

链路层:数据链路层,物理层
TCP/IP协议族中各协议之间的关系
对上图的说明:

网际层也称为:网络层;网络接口层也称为:链路层

端口

端口:可以认为是设备与外界通讯交流的出口。

端口号是唯一的

端口范围:0~65535

端口分类:

**公认端口(WellKnownPorts):**从0到1023,它们紧密绑定(binding)于一些服务。

**注册端口(RegisteredPorts):**从1024到49151。它们松散地绑定于一些服务。也就是说有许多服务绑定于这些端口,这些端口同样用于许多其它目的。例如:许多系统处理动态端口从1024左右开始。

**动态和/或私有端口(Dynamicand/orPrivatePorts):**从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。但也有例外:SUN的RPC端口从32768开始。

查看端口:

1、netstat -an指令查看

2、第三方扫描软件

IP地址

ip地址:用来在网络中标记一台电脑的一串数字,比如192.168.1.1;在本地局域网上是惟一的。

ip地址分类:

A类IP地址

一个A类IP地址由1字节的网络地址和3字节主机地址组成,网络地址的最高位必须是“0”,地址范围1.0.0.1-126.255.255.254二进制表示为:00000001 00000000 00000000 00000001 - 01111110 11111111 11111111 11111110
可用的A类网络有126个,每个网络能容纳1677214个主机

B类IP地址

一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是“10”,地址范围128.1.0.1-191.255.255.254
二进制表示为:10000000 00000001 00000000 00000001 - 10111111 11111111 11111111 11111110
可用的B类网络有16384个,每个网络能容纳65534主机

C类IP地址

一个C类IP地址由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是“110”
范围192.0.1.1-223.255.255.254
二进制表示为: 11000000 00000000 00000001 00000001 - 11011111 11111111 11111110 11111110
C类网络可达2097152个,每个网络能容纳254个主机

D类地址用于多点广播

D类IP地址第一个字节以“1110”开始,它是一个专门保留的地址。
它并不指向特定的网络,目前这一类地址被用在多点广播(Multicast)中
多点广播地址用来一次寻址一组计算机
地址范围224.0.0.1-239.255.255.254

  • E类IP地址*

以“1111”开始,为将来使用保留
E类地址保留,仅作实验和开发用

  • 私有ip*

在这么多网络IP中,国际规定有一部分IP地址是用于我们的局域网使用,也就
是属于私网IP,不在公网中使用的,它们的范围是:

10.0.0.0~10.255.255.255
172.16.0.0~172.31.255.255
192.168.0.0~192.168.255.255

注意

IP地址127.0.0.1~127.255.255.255用于回路测试,
如:127.0.0.1可以代表本机IP地址,用[http://127.0.0.1]
就可以测试本机中配置的Web服务器。

子网掩码

子网掩码(subnet mask)又叫网络掩码、地址掩码、子网络遮罩,

它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,

以及哪些位标识的是主机的位掩码。

子网掩码不能单独存在,它必须结合IP地址一起使用。

子网掩码只有一个作用:就是将某个IP地址划分成网络地址和主机地址两部分

与IP地址相同,子网掩码的长度也是32位,

左边是网络位,用二进制数字“1”表示;
右边是主机位,用二进制数字“0”表示。

假设IP地址为“192.168.1.1”子网掩码为“255.255.255.0”。

其中,“1”有24个,代表与此相对应的IP地址左边24位是网络号;
“0”有8个,代表与此相对应的IP地址右边8位是主机号。
这样,子网掩码就确定了一个IP地址的32位二进制数字中哪些是网络号、哪些是主机号。
这对于采用TCP/IP协议的网络来说非常重要,只有通过子网掩码,才能表明一台主机所在的子网与其他子网的关系,使网络正常工作。

子网掩码是“255.255.255.0”的网络:

最后面一个数字可以在0~255范围内任意变化,因此可以提供256个IP地址。
但是实际可用的IP地址数量是256-2,即254个,因为主机号不能全是“0”或全是“1”。

主机号全为0,表示网络号
主机号全为1,表示网络广播

注意:

如果将子网掩码设置过大,也就是说子网范围扩大,根据子网寻径规则,很可能发往和本地主机不在同一子网内的目标主机的数据,会因为错误的判断而认为目标主机是在同一子网内,导致数据包将在本子网内循环,直到超时并抛弃,使数据不能正确到达目标主机,导致网络传输错误;如果将子网掩码设置得过小,那么就会将本来属于同一子网内的机器之间的通信当做是跨子网传输,数据包都交给缺省网关处理,这样势必增加缺省网关的负担,造成网络效率下降。因此,子网掩码应该根据网络的规模进行设置。如果一个网络的规模不超过254台电脑,采用“255.255.255.0”作为子网掩码就可以了,现在大多数局域网都不会超过这个数字,因此“255.255.255.0”是最常用的IP地址子网掩码;假如在一所大学具有1500多台电脑,这种规模的局域网可以使用“255.255.0.0”。

socket

本地(一台机器上)的进程间通信(IPC)有很多种方式,例如

队列
同步(互斥锁、条件变量等)

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket(简称:套接字)。

Python提供了两个级别访问的网络服务:

低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统Socket接口的全部方法。

高级别的网络服务模块 SocketServer, 它提供了服务器中心类,可以简化网络服务器的开发。

Python创建socket:

import socket

socket.socket(family[, type[, protocol]])

参数解释:

family:套接字家族可以使AF_UNIX或者AF_INET

type:套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM

protocol:一般不填默认为0.

Socket 对象(内建)方法

服务器端套接字

s.bind()

绑定地址(host, port)到socket, 在AF_INET下,以元组(host, port的形式表示地址。)

s.listen()

开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。

s.accept()

被动接受TCP客户端连接,(阻塞式)等待连接的到来
返回为(socket object, address info)
其中socket object用于处理连接期间的通信,
包括服务器接收自客户端的信息,和服务器向客户端发送的信息。
区别于服务器套接字(server_socket)和客户端套接字(client_socket),
可以记为通信套接字(communication_socket)
address info是客户端的ip地址及端口(ip:port)

客户端套接字

s.connect()

主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

s.connect_ex()

connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数

s.recv()

接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。

s.send()

发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。

s.sendall()

完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

s.recvfrom(size)

接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

s.sendto(msg, addr)

发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。

s.close()

关闭套接字

s.getpeername()  

返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

s.getsockname()

返回套接字自己的地址。通常是一个元组(ipaddr,port)

s.setsockopt(level, optname, value)

设置给定套接字选项的值。

s.getsocketopt(level, optname, buflen)

返回套接字选项的值。

s.settimeout(timeout)

设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())

s.gettimeout()

返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。

s.gileno()

返回套接字的文件描述符。

s.setbolcking(flag)

如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常

s.makefile()

创建一个与该套接字相关连的文件

# 创建socket 例子
import socket

# 创建一个tcp socket
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print('tcp socket created!')

# 创建一个udp socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

print('udp socket created!')
tcp socket created!
udp socket created!
# socket 简单服务器
# 导入socket, sys 模块
import socket
import sys
from time import time
# 创建socket对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 指定绑定地址
host = ""
# 是指端口
port = 6666

#  绑定端口号
server_socket.bind((host, port))
# 设置最大连接数,超过后阻塞
server_socket.listen(5)
# 使用start_time、stop_time、run_time来控制循环运行
start_time = time() # 开始监听时间
run_time = 0 # 运行时间
while run_time < 10: # 运行时长:10s
    #  建立客户端连接
    client_socket, addr = server_socket.accept()
    
    print("client add is {}".format(addr))
    msg = "Hello this is server_socket send message!".encode("utf-8")
    client_socket.send(msg)
    client_socket.close()
    stop_time = time() # 停止时间
    run_time = stop_time - start_time # 总运行时间
    
print("Server exit.")
---------------------------------------------------------------------------

KeyboardInterrupt                         Traceback (most recent call last)

<ipython-input-1-ea5ca28be367> in <module>
     21 while run_time < 10: # 运行时长:10s
     22     #  建立客户端连接
---> 23     client_socket, addr = server_socket.accept()
     24 
     25     print("client add is {}".format(addr))


~/WorkStations/anaconda3/lib/python3.7/socket.py in accept(self)
    210         For IP sockets, the address info is a pair (hostaddr, port).
    211         """
--> 212         fd, addr = self._accept()
    213         sock = socket(self.family, self.type, self.proto, fileno=fd)
    214         # Issue #7995: if no default timeout is set and the listening


KeyboardInterrupt: 
# socket 客户端例子

# 导入socket, sys 模块
import socket
import sys

# 创建socket对象
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 指定服务器地址
host = "127.0.0.1"
# 是指端口
port = 6666
# 连接服务,指定主机和端口
s.connect((host, port))

# 接收小于2048字节的数据
msg = s.recv(2048)

s.close()

print(msg.decode('utf-8'))

socket 服务器 客户端 通信 结果图
socket 服务器 客户端 通信 结果图#### UDP

UDP — 用户数据报协议,是一个无连接的简单的面向数据报的运输层协议。

udp是TCP/IP协议族中的一种协议能够完成不同机器上的程序间的数据通信

udp只是把应用层传给ip层的数据报发出去,不能保证到达;udp在传输数据报前不用在客户和服务器之间建立连接,没有超时重发等机制,传输速度快。

UDP是一种面向无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。

UDP特点:

面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。

UDP传输数据有大小限制:每个被传输的数据报必须限定在64KB内。

udp不可靠,发送方所发送的数据报并不一定以相同的次序到达接收方。

UDP一般用于多点通信和实时的数据业务:

 语音广播
 
视频

QQ

TFTP(简单文件传送)

SNMP(简单网络管理协议)

RIP(路由信息协议,如报告股票市场,航空信息)

DNS(域名解释)

UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。

udp端口号动态变化解释:
每次重新运行网络程序,对于未绑定端口号的程序,系统默认随机分配一个端口号来唯一标识这个程序。如果需要向此程序发送信息,只需要向这个端口标识的程序发送即可。

UDP网络通信过程
UDP网络通信过程
udp服务器、客户端

udp的服务器和客户端的区分:往往是通过请求服务和提供服务来进行区分

请求服务的一方称为:客户端

提供服务的一方称为:服务器

udp 发送数据:

创建一个udp客户端程序步骤:

1、创建客户端套接字

2、发送/接收数据

3、关闭套接字

UDP创建流程图
UDP创建流程图

# 创建udp 客户端 例子udp
# -*- coding: utf-8 -*-
import socket
# 创建客户端socket
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 服务器地址
server_addr = ("192.168.1.1", 2333)
# 客户端向服务器发送的信息
# 记得编码【encode()】
client_data = "hello".encode("utf-8")
# 向服务器发送client_data数据
client_socket.sendto(client_data, server_addr)
# 接收服务器发送的信息(数据,地址)
msg, addr = client_socket.recvfrom(1024)
# 打印接收自服务器的信息msg, 服务器地址:addr
print("server message is:\n\t{}\nserver address is:{}\n\t".format(
    msg.decode("utf-8"), addr))
# 关闭连接
client_socket.close()
# udp 服务器
# -*- coding: utf-8 -*-
import socket

# 创建服务器socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 设置绑定地址
listen_addr = ("", 2333)
# 绑定地址
server_socket.bind(listen_addr)

# 接收客户端信息msg,客户端地址addr
msg, addr = server_socket.recvfrom(1024)

# 打印客户发送的数据
print("receive message from client:\n\t{}".format(msg.decode("utf-8")))
# 服务器向客户端发送的数据
server_data = "this is server_data".encode("utf-8")
# 向客户端addr,发送数据server_data
server_socket.sendto(server_data, addr)
# 关闭连接
server_socket.close()
udp绑定

一个udp网络程序,可以不绑定,此时操作系统会随机进行分配一个端口,

如果重新运行次程序端口可能会发生变化

一个udp网络程序,也可以绑定信息(ip地址,端口号),如果绑定成功,

那么操作系统用这个端口号来进行区别收到的网络数据是否是此进程的

# udp 接收方 端口绑定 例子
# -*-coding: utf-8 -*-
import socket

# 1、创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2、绑定本地的相关信息,若一个网络程序不绑定,则系统会随机分配端口号
bind_addr = ('',6666)# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(bind_addr)
# 3、等待接收数据
recv_data = udp_socket.recvfrom(1024)# 1204为本次接收的最大字节数
# 4、显示接收数据
# 若接收到的是bite格式,则用decode('decode-type')解码,str类型不必decode
print(recv_data.decode('utf-8'))
# 5、关闭socket
udp_socket.close()

# 因为例子是接收方的端口绑定,所以单独运行是没有显示的,故不运行
# udp echo 服务器 示例
# echo服务器:将收到的信息原封不动的返回
import socket

# 1、创建socket
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 2、绑定本地相关信息
bind_addr = ('', 6666)# ip地址和端口号,ip一般不用写,表示本机的任何一个ip
udp_socket.bind(bind_addr)

# 统计次数
num = 0

while True:
    # 3、等待接收对方发送的数据
    recv_data = udp_socket.recvfrom(2048)# 最大接收字节数2048
    # 4、回传接收到的信息
    udp_socket.sendto(recv_data[0], recv_data[1])
    # 5、统计信息
    num +=1
    print('已完成{}次数据收发'.format(num))
    
# 5、关闭socket
udp_socket.close()
udp广播
# udp 广播例子
# -*- coding: utf-8 -*-

import socket, sys
from time import time
dest = ('<broadcast>', 6666)

# 创建socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 对socket进行修改,以发送广播
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

# 以广播的形式发送数据到网络的所有电脑中
s.sendto('hello, world!'.encode("utf-8"), dest)
print('等待回复,Ctrl+c退出')

start_time = time()
run_time = 0
while run_time < 10:
    (buf, address) = s.recvfrom(2048)
    print("received from {}:{}".format(address, buf))
    stop_time = time()
    run_time = stop_time - start_time
等待回复,Ctrl+c退出

TCP

tcp通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中"打电话"

TCP三次握手和四次挥手部分参考自下面的网址:

参考网址 作者:小书go

背景描述:

网络层,可以实现两个主机之间的通信。但是这并不具体,因为,真正进行通信的实体是在主机中的进程,是一个主机中的一个进程与另外一个主机中的一个进程在交换数据。IP协议虽然能把数据报文送到目的主机,但是并没有交付给主机的具体应用进程。而端到端的通信才应该是应用进程之间的通信。

UDP,在传送数据前不需要先建立连接,远地的主机在收到UDP报文后也不需要给出任何确认。虽然UDP不提供可靠交付,但是正是因为这样,省去和很多的开销,使得它的速度比较快,比如一些对实时性要求较高的服务,就常常使用的是UDP。对应的应用层的协议主要有 DNS,TFTP,DHCP,SNMP,NFS 等。

TCP,提供面向连接的服务,在传送数据之前必须先建立连接,数据传送完成后要释放连接。因此TCP是一种可靠的的运输服务,但是正因为这样,不可避免的增加了许多的开销,比如确认,流量控制等。对应的应用层的协议主要有 SMTP,TELNET,HTTP,FTP 等。

TCP报文首部

  1. 源端口和目的端口,各占2个字节,分别写入源端口和目的端口;

  2. 序号,占4个字节,TCP连接中传送的字节流中的每个字节都按顺序编号。例如,一段报文的序号字段值是 301 ,而携带的数据共有100字段,显然下一个报文段(如果还有的话)的数据序号应该从401开始;

  3. 确认号,占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是501,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;

  4. 数据偏移,占4位,它指出TCP报文的数据距离TCP报文段的起始处有多远;

  5. 保留,占6位,保留今后使用,但目前应该都位0;

  6. 紧急URG,当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;

  7. 确认ACK,仅当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;

  8. 推送PSH,当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;

  9. 复位RST,当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接;

  10. 同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;

  11. 终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;

  12. 窗口,占2字节,指的是通知接收方,发送本报文你需要有多大的空间来接受;

  13. 检验和,占2字节,校验首部和数据这两部分;

  14. 紧急指针,占2字节,指出本报文段中的紧急数据的字节数;

  15. 选项,长度可变,定义一些其他的可选的参数。

TCP通信过程TCP通信过程##### TCP三次握手
TCP三次握手

syn>>>> syn+ack<<<<<<<<<<<< ack>>>>>>>>>>>>>>>

*tcp传输前会发送ack包确认,但是udp不会进行确认,所以有tcp比udp稳定

client close()方法,会通知到server,server会回复通知信息,然后client回复,挥手完*
TCP三次握手图解>图片作者:小书go
感觉作者关于TCP的三次握手与四次挥手写的比我好,请移步作者博客

作者博客地址:导向链接
https://blog.csdn.net/qzcsu/article/details/72861891

最开始的时候客户端和服务器都是处于CLOSED状态。主动打开连接的为客户端,被动打开连接的是服务器。

  1. TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;

  2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。

  3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。

  4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。

为什么TCP客户端最后还要发送一次确认

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

tcp四次挥手

tcp四次挥手tcp四次挥手图解过程>图片作者:小书go
感觉作者关于TCP的三次握手与四次挥手写的比我好,请移步作者博客

作者博客地址:导向链接
https://blog.csdn.net/qzcsu/article/details/72861891

数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED状态,然后客户端主动关闭,服务器被动关闭。

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

tcp十种状态
TCP十种状态
当一端收到一个FIN,内核让read返回0来通知应用层另一端已经终止了向本端的数据传送

发送FIN通常是应用层对socket进行关闭的结果
TCP十种状态

TCP的2MSL问题

TCP2MSL问题

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

TCP长连接和短连接

TCP在真正的读写操作之前,server与client之间必须建立一个连接,
当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,
连接的建立通过三次握手,释放则需要四次握手,
所以说每个连接的建立都是需要资源消耗和时间消耗的。

TCP短连接

模拟一种TCP短连接的情况:

  1. client 向 server 发起连接请求

  2. server 接到请求,双方建立连接

  3. client 向 server 发送消息

  4. server 回应 client

  5. 一次读写完成,此时双方任何一个都可以发起 close 操作

在第 步骤5中,一般都是 client 先发起 close 操作。当然也不排除有特殊的情况。
从上面的描述看,短连接一般只会在 client/server 间传递一次读写操作!

TCP长连接

模拟一种TCP长连接的情况:

  1. client 向 server 发起连接请求

  2. server 接到请求,双方建立连接

  3. client 向 server 发送消息

  4. server 回应 client

  5. 一次读写完成,连接不关闭

  6. 后续读写操作…

  7. 长时间操作之后client发起关闭请求

TCP长/短连接操作过程

短连接:

建立连接——数据传输——关闭连接…建立连接——数据传输——关闭连接
短连接操作过程长连接:
建立连接——数据传输…(保持连接)…数据传输——关闭连接
长连接操作过程
长短连接各自优缺点:
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。
对于频繁请求资源的客户来说,较适用长连接。

client与server之间的连接如果一直不关闭的话,会存在一个问题,
随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,
如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;
如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,
这样可以完全避免某个蛋疼的客户端连累后端服务。

短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。

但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。

TCP长/短连接的应用场景:

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,

常见网络攻击案例

tcp半链接攻击

tcp半链接攻击也称为:SYN Flood (SYN洪水)
是种典型的DoS (Denial of Service,拒绝服务) 攻击
效果就是服务器TCP连接资源耗尽,停止响应正常的TCP连接请求

dns攻击

dns服务器被劫持

域名服务器对其区域内的用户解析请求负责,但是并没有一个机制去监督它有没有真地负责。
将用户引向一个错误的目标地址。这就叫作 DNS 劫持,主要用来阻止用户访问某些特定的网站,或者是将用户引导到广告页面。

dns欺骗

DNS 欺骗简单来说就是用一个假的 DNS 应答来欺骗用户计算机,让其相信这个假的地址,并且抛弃真正的 DNS 应答。

arp攻击
arp攻击

TCP服务器

创建TCP服务器过程:

1、创建socket

2、bind绑定ip和port

3、listen使套接字变为可以被动链接

4accept等待客户端的链接

5、recv/send接收发送数据

TCP服务器创建流程:socket(), bind(), listen(), accept(), read(), write(), close()

# tcp服务器 示例代码
# -*- coding: utf -8 -*-

import socket

# 创建socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定本地信息
address = ("", 6666)
tcp_server_socket.bind(address)

# 使用socket创建的套接字默认的属性是主动的,使用listen变为被动
# 变为被动后,就可以接收别人的链接了
tcp_server_socket.listen(5)

# 有行的客户端链接服务器,就为这个客户端生成一个新的套接字
# new_socket用来为这个客户端服务
# tcp_server_socket可以省下等待其他新的客户端链接
new_socket, client_addr = tcp_server_socket.accept()

# 接收对方发送的数据,最大为1024字节
recv_data = new_socket.recv(1024)
print('接收到的数据为:{}'.format(recv_data))

# 发送数据到客户端
new_socket.send('send from server!')

# 关闭为此客户端创建的socket
# 关闭后就不再服务,要想继续服务只能重连
new_socket.close()

# 关闭监听socket,此socket关闭表示不再接收客户端链接
tcp_server_socket.close()
TCP客户端
# tcp 客户端 示例代码
# -*- coding: utf-8 -*-
import socket

# 创建socket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 链接服务器
server_addr = ('192.168.1.1', 6666)
tcp_client_socket.connect(server_addr)

# 提示输入数据
send_data = input('请输入要发送的数据:')

# 接收服务器发送来的数据,最大接收1024字节
recv_data = tcp_client_socket.recv(1024)
print('接收到的数据:{}'.format(recv_data))

# 关闭socket
tcp_client_socket.close()
'''tcp 模拟qq'''
# 客户端代码
# -*- coding: utf-8 -*-
import socket

# 创建secket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接服务器
server_addr = ('192.168.1.1', 6666)
tcp_client_socket.connect(server_addr)

while True:
    # 提示用户输入数据
    send_data = input('send: ')
    if len(send_data)>0:
        tcp_client_socket.send(send_data)
    else:
        break
        
    # 接收对方发来的数据,最大接收1024字节
    recv_data = tcp_client_socket.recv(1024)
    print('received:', recv_data)
    
# 关闭套接字
tcp_client_socket.close()

# 服务器 实例代码
#-*- coding: utf-8 -*-
import socket

# create socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 绑定本地信息
address = ('', 6666)
tcp_server_socket.bind(address)

#用listen变socket为被动
tcp_server_socket.listen(5)

while True:
    # new socket
    new_socket, client_addr = tcp_server_socket.accept()
    
    while True:
        # 接收数据,最大接收1024字节
        recv_data = new_socket.recv(1024)
        
        # 接收到的数据长度为0,表示客户端关闭链接
        if len(recv_data)>0:
            print('recv: ', recvData)
            
        else:
            break
        
        # 发送数据到客户端
        send_data = input('send: ')
        new_socket.send(send_data)
        
    # 关闭此客户端的套接字,不再为此客户端服务
    new_socket.close()
        
# 关闭监听socket
tcp_server_socket.close()

网络编程 应用

单全双工信息收发

单工:只能接收,半双工:同一时刻只能接收或是发送,全双工:可接可发

socket(套接字)是全双工的

编码:encode(“utf-8”)

解码:decode(“utf-8”)

# 简单全双工信息收发 例子
#-*-coding: utf-8 -*-

from threading import Thread

import socket

# 接收数据,打印
def recv_data():
    while True:
        recv_msg = udp_socket.recvfrom(4096)
        print('收到>:{}:()'.str(recv_msg[1], recv_msg[0]))
        
# 发送数据
def send_data():
    while True:
        send_data = input('发送\r<:')
        print('>:')
        udp_socket.sendto(send_data.encode('utf-8'), (which_ip, which_port))
        
# socket, ip, port
udp_socket = None
which_ip = ''
which_port = 0

def main():
    global udp_socket
    global which_ip
    global which_port
    
    # 输入ip:port
    which_ip = input('请输入对方IP:\t')
    which_port = input("请输入对方的port:\t")
    # create socket
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # bind local info
    udp_socket.bind('', 6666)
    
    # create threading
    tr = Thread(target=recv_data)
    ts = Thread(target=send_data)
    
    # start threading
    tr.start()
    ts.start()
    
    # blocking
    tr.join()
    ts.join()
    
if __name__=='__main__':
    main()
TFTP客户端

TFTP 协议介绍

TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议

TFTP特点:

简单

占用资源小

适合传递小文件

适合在局域网进行传递

端口号为69

基于UDP实现

TFTP下载过程

TFTP服务器默认监听69号端口

当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送

服务器若批准此请求,则使用一个新的、临时的端口进行数据传输
TFTP下载过程
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端

如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来

因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的

因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面

操作码 功能
1 读请求(下载)
2 写请求(上传)
3 表示数据包(DATA)
4 确认码(ACK)
5 错误

TFTP协议中规定,服务器确认客户端收到刚刚发送的数据包:当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,这样的包成为ACK(应答包)

发送完:客户端接收到的数据小于516(2字节操作码+2字节序号+512字节数据)
TFTP数据包的格式

'''
同一时刻只能为一个客户进行服务,不能同时为多个客户服务
类似于找一个“明星”签字一样,客户需要耐心等待才可以获取到服务
当服务器为一个客户端服务时,而另外的客户端发起了connect,
只要服务器listen的队列有空闲的位置,就会为这个新客户端进行连接,
并且客户端可以发送数据,但当服务器为这个新客户端服务时,
可能一次性把所有数据接收完毕
当recv接收数据时,返回值为空,即没有返回数据,
那么意味着客户端已经调用了close关闭了;
因此服务器通过判断recv接收数据是否为空 来判断客户端是否已经下线
'''

# tftp 客户端 示例代码
# -*- coding:utf-8 -*-

import struct

from socket import *

import time

import os

def main():
    #0. 获取要下载的文件名字:
    downloadFileName = raw_input("请输入要下载的文件名:")
    #1.创建socket
    udpSocket = socket(AF_INET, SOCK_DGRAM)
    # 这里对下面的!H8sb5sb解释:
    # !H占两个,表示操作码,8s是文件名的占位长度,b是0的占位长度,5sb同理
    requestFileData = struct.pack("!H%dsb5sb"%len(downloadFileName), 1, downloadFileName, 0, "octet", 0)
    #2. 发送下载文件的请求
    udpSocket.sendto(requestFileData, ("192.168.119.215", 69))
    flag = True #表示能够下载数据,即不擅长,如果是false那么就删除
    num = 0
    f = open(downloadFileName, "w")

    while True:
        #3. 接收服务发送回来的应答数据
        responseData = udpSocket.recvfrom(1024)
        # print(responseData)
        recvData, serverInfo = responseData
        opNum = struct.unpack("!H", recvData[:2])
        packetNum = struct.unpack("!H", recvData[2:4])
        print(packetNum[0])
        # print("opNum=%d"%opNum)
        # print(opNum)
        # if 如果服务器发送过来的是文件的内容的话:
        if opNum[0] == 3: #因为opNum此时是一个元组(3,),所以需要使用下标来提取某个数据
            #计算出这次应该接收到的文件的序号值,应该是上一次接收到的值的基础上+1
            num = num + 1
            # 如果一个下载的文件特别大,即接收到的数据包编号超过了2个字节的大小
            # 那么会从0继续开始,所以这里需要判断,如果超过了65535 那么就改为0
            if num==65536:
                num = 0
            # 判断这次接收到的数据的包编号是否是 上一次的包编号的下一个
            # 如果是才会写入到文件中,否则不能写入(因为会重复)
            if num == packetNum[0]:
                # 把收到的数据写入到文件中
                f.write(recvData[4:])
                num = packetNum[0]
                #整理ACK的数据包
                ackData = struct.pack("!HH", 4, packetNum[0])
                udpSocket.sendto(ackData, serverInfo)
        elif opNum[0] == 5:
            print("sorry,没有这个文件....")
            flag = False
        # time.sleep(0.1)
        if len(recvData)<516:
            break
        if flag == True:
            f.close()
        else:
            os.unlink(downloadFileName)#如果没有要下载的文件,那么就需要把刚刚创建的文件进行删除

if __name__ == '__main__':
    main()
单进程 TCP服务器
# 单进程 TCP服务器
from socket import *

server_socket = socket(AF_INET, SOCK_STREAM)

# 重用绑定的信息
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

local_addr = ('', 6666)

server_socket.bind(local_addr)

server_socket.listen(5)

while True:
    print('主进程,等待新客户端的到来')
    new_socket, dest_addr = server_socket.accept()
    print('主进程, 接下来负责数据处理{}'.format(str_dest_addr))
    
    try:
        while True:
            recv_data = new_socket.recv(1024)
            if len(recv_data)>0:
                print('recv[{}]:{}'.format(str(dest_addr), recv_data))
            else:
                print('{}客户端已经关闭'.format(str(dest_addr)))
                
    finally:
        new_socket.close()

server_socket.close()
主进程,等待新客户端的到来
多进程服务器
# 多进程服务器
'''
通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务
当客户端不是特别多的时候,这种方式还行,如果有几百上千个,就不可取了,
因为每次创建进程等过程需要较大的资源
'''
import socket

from multiprocessing import Process

from time import sleep

# 处理客户端请求,并为其服务
def deal_with_client(new_socket, dest_addr):
    while True:
        recv_data = enw_socket.recv(1024)
        
        if len(recv_data)>0:
            print('recv[{}]: {}'.format(str(dest_addr), recv_data))
        else:
            print('[{}]客户端已经关闭'.format(str(dest_addr)))
            break
    new_socket.close()
        
def main():
    server_socket = socket(AF_INET, SOCK_STREAM)
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    local_addr = ('', 6666)
    server_socket.bind(local_addr)
    server_socket.listen(5)
    
    try:
        while True:
            print('主进程,等待新客户端的到来')
            new_socket, dest_addr = server_socket.accept()
            
            print('主进程, 接下来创建一个新的进程负责数据处理[{}]'.format(str(dest_addr)))
            client = Process(target=deal_with_client, args=(new_socket, dest_addr))
            client.start()
            
            # 因为已经向子进程copy了一份(引用),并且父进程中这个套接字也没有用处了
            # 所以关闭
            new_socket.close()
    finally:
        3 当为所有的客户端服务完后再关闭,表示不再接收新的客户端的连接
        server_socket.close()
        
if __name__=='__main__':
    main()
多线程服务器
# 多线程服务器
# -*- coding: utf-8 -*-

from socket import *

from threading import Thread

from thime import sleep

# 处理客户的请求并执行事情
def deal_with_client(new_socket, dest_addr):
    while True:
        recv_data = new_socket.recv(1024)
        
        if len(recv_data)>0:
            print('recv[{}]: {}'.format(str(dest_addr), recv_data))
            
        else:
            print('[{}]客户端已经关闭'.format(dest_addr))
            break
        
        new_socket.close()
        
def main():
    server_socket = socket(AF_INET, SOCK_STREAM)
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    local_addr = ('',6666)
    server_socket.bind(local_addr)
    server_socket.listen(5)
    
    try:
        while True:
            print('-----主进程,,等待新客户端的到来------')
            new_socket, dest_addr = server_socket.accept()
            
            print('主进程,接下来创捷一个新的进程负责数据处理[{}]'.format(
                str(dest_addr)
            ))
            client = Thread(target=deal_with_client, args=(new_socket, dest_addr))
            client.start()
            
            # 线程中共享socket, 若关闭会导致套接字不可用
            # 但是此时在线程中这个socket可能还在收数据,因此不可关闭
            # new_socket.close()
            
    finally:
        server_socket.close()

if __name__ =='__main__':
    main()
单进程服务器,非阻塞模式
# 服务器
# -*- coding: utf-8 -*-

from socket import *
import time

# 存储所有的新连接的socket
g_socket_list = []

def main():
    server_socket = socket(AF_INET, SOCK_STREAM)
    server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    local_addr = ('', 6666)
    server_socket.bind(local_addr)
    # 可以适当修改listen中的值来看看不同的现象
    server_socket.listen(1000)
    # 设置套接字为非阻塞
    # 设置为非阻塞后,若accept时,无客户端connect,accept会抛出一个异常,
    # 所以需要try处理异常
    server_socket.setblocking(False)
    
    while True:
        # 测试
        # time.sleep(0.5)
        
        try:
            new_client_info = server_socket.accept()
        except Exception as result:
            pass
        else:
            print('一个新的客户端到来:{}'.format(str(new_client_info))
            new_client_infp[0].setblocking(False)
            g_socket_list.append(new_client_info)
        # 存储需要删除的客户端信息
        need_del_client_info_list = []
        
        for client_socket, client_addr in g_socket_list:
            try:
                recv_data = client_socket.recv(1024)
                if len(recv_data)>0:
                    print('recv[{}]:{}'.format(str(client_addr), recv_data))
                else:
                    print('[{}]客户端已经关闭'.format(client_addr))
                    client_socket.close()
                    g_need_del_client_info_list.append(client_socket, client_addr)
            except Exception as result:
                  pass
                  
        for need_del_client_info in need_del_client_info_list:
            g_socket_list.remove(need_del_client_info)

if __name__=='__main__':
    main()
                  
# 客户端
#coding=utf-8
from socket import *
import random
import time

serverIp = input("请输入服务器的ip:")
connNum = input("请输入要链接服务器的次数(例如1000):")
g_socketList = []
for i in range(int(connNum)):
    s = socket(AF_INET, SOCK_STREAM)
    s.connect((serverIp, 7788))
    g_socketList.append(s)
    print(i)

while True:
    for s in g_socketList:
        s.send(str(random.randint(0,100)))

    # 用来测试用
    #time.sleep(1)
select版 TCP服务器

select原理

多路复用的模型中,较常用的有select,epoll模型。这两个都是系统接口,
由操作系统提供。当然,Python的select模块进行了更高级的封装。

网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。
这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于的文件描述符可读则会返回改文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select让用户进程则会睡眠,直到等待资源可用的时候在唤醒,遍历之前那个监视的数组。每次遍历都是依次进行判断的。

select 优缺点

优点

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。
当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

# select 回显服务器
# 当客户端向服务器发送数据后,服务器会把客户端发送的数据发回客户端
# 当服务器端键入数据后,打印键入的数据,关闭服务器
# 当服务器运行超过30s后,自动关闭服务器
import select
import socket
import sys
from time import time

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('', 6660))
server.listen(5)

inputs = [server, sys.stdin]

running = True

# 设置服务时间控制
start_time = time()
run_time = 0

while run_time <30:
    # 调用select函数,阻塞等待
    readable, writeable, exceptional = select.select(inputs, [], [])
    flag = False # 键入数据后关闭服务器
    
    # 数据到达,循环
    for sock in readable:
        # 监听到新的连接
        if sock == server:
            conn, addr = server.accept()
            # select 监听的socket
            inputs.append(conn)
            # 监听到键盘有输入
        elif sock == sys.stdin:
            cmd = sys.stdin.readline()
            print(
                "find the data input. The input is:\n\t{}\nThe server will EXIT."
                .format(cmd))
            flag = True
            running = False
            break
            
        # 有数据到达
        else:
            # 读取客户端连接发送的数据
            data = sock.recv(1024)
            if data:
                sock.send(data)
            else:
                # 移除select监听的socket
                inputs.remove(sock)
                sock.close()
        # 如果检测到用户输入敲击键盘,那么就退出
        if not running:
            break
    if flag:
        break
    stop_time = time()
    run_time = stop_time - start_time
            
server.close()
        
# select回显服务器测试客户端代码
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import socket
from time import time

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_addr = ('127.0.0.1', 6666)
client_socket.connect_ex(server_addr)
msg = "hello world".encode("utf-8")

client_socket.send(msg)
recv_data = client_socket.recv(1024)
print("client receive data is:\n\t {}".format(recv_data.decode("utf-8")))
client_socket.close()
# 包含writeList服务器
# -*- coding: utf-8 -*-
import socket
import Queue
form select import select

server_ip = ('', 8888)

# 保存客户端发过来的消息,存入消息队列
message_queue = {}
input_list = []
output_list = []

if __name__ == '__main__':
    server = socket.socket()
    server.bind(server_ip)
    server.listen(10)
    # 设置为非阻塞
    server.setblocking(False)
    # 初始化将服务器加入监听列表
    input_list.append(server)
    
    while True:
        # 开始select监听,对input_list中的服务端server进行监听
        stdinput, stdoutput, stderr = select(inut_list, out_list, input_list)
        
        # 循环判断是否有客户端连接进来,有客户端连接时select将触发
        for obj in stdinput:
            # 判断当前出发的是不是服务端对象,当触发的对象是服务端对象时,
            # 说明有新客户连接进来了
            if obj == server:
                # 接收客户端的连接, 获取客户端对象和客户端地址信息
                
                conn, addr = server.accept()
                
                print('client {} connected!'.format(addr))
                # 将客户端对象也加入到监听的列表中,当客户端发送消息时select将触发
                input_list.append(conn)
                # 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
                message_queue[conn] = Queue.Queue()
            else:
                # 将客户端加入到了监听列表(input_list),客户端发送消息就触发
                try:
                    recv_data = obj.recv(1024)
                    # 客户端未断开
                    print('received {} from client {} '.format(recv_data, str(addr)))
                    # 将收到的消息放入到各客户端的消息队列中
                    message_queue[obj].put(recv_data)
                    # 将回复操作放到output列表中,让select监听
                    if obj not in output_list:
                        output_list.append(obj)
                
                except ConnectionResetError:
                    # 客户端断开连接了,将客户端的监听中input列表中移除
                    input_list.remove(obj)
                    # 移除客户端的消息队列
                    del message_queue[obj]
                    print("\n[input] Client %s disconnected"%str(addr))
        # 如果现在没有客户端请求,也没有客户端发送消息时,
        # 开始对发送消息队列进行处理,是否需要发送消息
        for sendobj in output_list:
            try:
                # 如果消息队列中有消息,从消息队列中获取要发送的消息
                if not message_queue[sendobj].empty():
                    # 从该客户端对象的消息队列中获取要发送的消息
                    send_data = message_queue[sendobj].get()
                    sendobj.send(send_data)
                else:
                    # 将监听移除等待下一次客户端发送消息
                    output_list.remove(sendobj)
            except ConnectResetError:
                # 客户端连接断开
                del message_queue[sendobj]
                output_list.remove(sendobj)
                print("\n[output] Client  %s disconnected"%str(addr))
epoll版-TCP服务器

epoll的优点

没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。

epoll使用说明

EPOLLIN (可读)
EPOLLOUT (可写)
EPOLLET (ET模式)

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。

ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

# epoll 参考代码

import socket
import select
from select import epoll

# 创建socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定本机信息
s.bind('', 1234)

# 绑定本机信息
s.listen(10)

# 创建一个epoll对象
# 测试,用来打印套接字对应的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET

# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)

connections = {}
addresses = {}

# 循环等待客户端的到来或者对方发送数据
while True:
    # epoll 进行fd扫描的地方 -- 未指定超时时间为阻塞等待
    epoll_list = epoll.poll()
    
    # 对事件进行判断
    for fd, events in epoll_list:
        # print fd
        # print event
    
        # 如果是socket创建的socket被激活
        if fd == s.fileno():
            conn, addr = s.accept()
            
            print('有新的客户端到来{}'.format(str(addr)))
            
            # 将conn和addr信息分别保存起来
            connections[conn.fileno()] = conn
            addresses[conn.fileno()] = addr
            
            # 向epoll中注册连接socket 的可读事件
            epoll.register(conn.fileno(), select.EPOLLIN|select.EPOLLET)
            
        elif events == select.EPOLLIN:
            # 从激活fd上接收
            recv_data = connections[fd].recv(1024)
            
            if len(recv_data)>0:
                print('recv: {} '.format(recv_data))
                
            else:
                # 从epoll中移除该连接 fd
                epoll.unregister(fd)
                # server 侧主动关闭该连接 fd
                connections[fd].close()
                
                print('{} ---offline---'.format(str(addresses[fd])))
# gevent版TCP服务器

import sys

import time

import gevent

from gevent import socket, monkey

monkey.patch_all()

def handle_request(conn):
    data = conn.recv(1024)
    if not data:
        conn.close()
        break
        
    print('recv: {}'.format(data))
    conn.send(data)
    
def server(port):
    s = socket.socket()
    s.bind(('', port))
    s.listen(5)
    
    while True:
        cli, addr = s.accept()
        gevent,spawn(handle_request, cli)
        
if __name__ == '__main__':
    server(4567)
  File "<ipython-input-1-a04bdaa77df3>", line 17
    break
    ^
SyntaxError: 'break' outside loop



C:\MyPrograms\Anaconda3\lib\site-packages\gevent\hub.py:154: UserWarning: libuv only supports millisecond timer resolution; all times less will be set to 1 ms
  with loop.timer(seconds, ref=ref) as t:
发布了13 篇原创文章 · 获赞 0 · 访问量 22

猜你喜欢

转载自blog.csdn.net/qq_34764582/article/details/104940890