Linux相关命令

  • ifconfig:查看网卡信息
  • sudo ifconfig ens33 ip:修改网卡ens33的ip地址
  • ping ip/域名:检测网络是否正常
  • route -n:查看路由
  • netstat -an:查看端口状态
  • sudo lsof -i [tcp/udp]:端口号:查看哪一个进程用了该端口
  • ps -elf|grep udp_server:查看某个进程是否还在
  • kill -9 进程id:杀进程

    Socket简介

  • socket(套接字)是进程间通信的一种方式,它与其他进程间通信的一个主要不同是:它能实现不同主机间的进程间通信

  • 套接字的使用流程
    1. 创建套接字
    2. 使用套接字收 / 发数据
    3. 关闭套接字
  • 创建socket

image.png

  • AddressFamily:可以选择AF_INET(用于Internet进程间通信)或者AF_UNIX(用于同一台机器进程间通信),实际工作中常用AF_INET
  • Type:套接字类型,可以是SOCK_STREAM(流式套接字,主要用于TCP协议)或者SOCK_DGRAM(数据报套接字,主要用于UDP协议)
    • 服务器端套接字的方法
  • s.bind():绑定地址(host, port)到套接字,在AF_INET下,以元组的形式表示地址
  • s.listen():开始TCP监听,backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量,该值至少为1,大部分应用程序设为5就可以了
  • s.accept():被动接受TCP客户端连接,阻塞式等待连接的到来
    • 客户端套接字的方法
  • s.connect():主动初始化TCP服务器连接,一般地址的格式为元组(host, port),如果连接出错,返回socket.error异常
  • s.connect_ex():connect()方法的扩展版本,出错时返回出错码,而不是抛出异常
    • 共用的方法
  • s.send():发送TCP数据,将string中的数据发送到连接的套接字,返回值是要发送的字节数,该数值可能小于string的字节大小
  • s.sendall():完整发送TCP数据,将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据,成功返回None,失败则抛出异常
  • s.recv():接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量,flag提供有关消息的其他信息,通常可以忽略
  • s.sendto():发送UDP数据,返回值是发送的字节数
  • s.recvfrom():接收UDP数据,返回值是(data, socket),其中data是包含接收数据的字符串,socket是发送端的套接字
  • s.close():关闭套接字
  • 其他:python网络编程

    UDP通信

    User Datagram Protocol:用户数据报协议

    通信结构图

    9. 网络编程 - 图2

    编程说明

  • 服务器端的ip地址必须是本机地址,否则会报错
  • 服务器端的端口号需要指定
    • 说明:端口号只有整数,范围是0~65535,知名端口的范围是0~1023,动态端口的范围是1024~65535,动态端口表示它一般不固定分配给某种服务
    • 举例

image.png

  • 客户端的端口号可以不用指定,每次连接系统默认会随机分配,ip地址也不需要指定
  • 发送数据需要进行编码转换
    • encode编码:str —> bytes,例如 ‘helloworld’.encode(‘utf8’)
    • decode解码:bytes —> str,例如 recv_data[0].decode(‘utf8’)
  • recvfrom()方法中的参数表示接收的最大字节数,如果该数小于另一端发送的字节数,Windows会报错,Linux不会报错,接收不下的数据直接丢弃

    举例

    ```python import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_addr = (‘127.0.0.1’, 2000)

client_socket.sendto(‘helloworld’.encode(‘utf8’), server_addr)

recv_data = client_socket.recvfrom(50) print(recv_data[0].decode(‘utf8’))

client_socket.close()

  1. ```python
  2. import socket
  3. server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  4. server_addr = ('127.0.0.1', 2000)
  5. server_socket.bind(server_addr)
  6. recv_data = server_socket.recvfrom(50)
  7. print(recv_data[0].decode('utf8')) # 显示接收到的数据
  8. print(recv_data[1]) # 显示对方的ip地址和端口号
  9. server_socket.sendto('Ilovepython'.encode('utf8'), recv_data[1])
  10. server_socket.close()

TCP通信

Transmission Control Protocol:传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP通信需要经过创建连接、传送数据、终止连接三个步骤

特点

  • 面向连接
    • 通信双方必须先建立连接才能进行数据的传输,双方都必须为该连接分配必要的系统内核资源,以管理连接的状态和连接上的传输
    • 双方间的数据传输都可以通过这一个连接进行
    • 完成数据交换后,双方必须断开此连接,以释放系统资源
    • 这种连接是一对一的,因此TCP不适用于广播的应用程序,基于广播的应用程序需要使用UDP协议
  • 可靠传输

    • TCP采用发送应答机制:每一个报文都有一个序列号,TCP发送的每个报文段都必须得到接收方的应答才认为这个TCP报文段传输成功
    • 超时重传:发送端发出一个报文段之后就启动定时器,如果在定时时间内没有收到应答就重新发送这个报文段
    • 错误检验:TCP用一个校验和函数来检验数据是否有错误,在发送和接收时都要计算校验和
    • 流量控制和阻塞管理:使用滑动窗口机制来控制双方的发送接收速度,用于避免主机发送得过快而使接收方来不及完全收下

      TCP和UDP的区别

      9. 网络编程 - 图4

      三次握手

      关于三次握手和四次挥手的常见面试题
      image.png

      四次挥手

      image.png

      长连接和短连接

  • 一种短连接的情况

    1. client向server发起连接请求
    2. server接到请求,双方建立连接
    3. client向server发送信息
    4. server回应client
    5. 一次读写完成后,双方任何一个都可以发起close操作
    • 说明:一般都是client先发起close操作,短连接一般只会传递一次读写操作
  • 一种长连接的情况
    1. client向server发起连接请求
    2. server接到请求,双方建立连接
    3. client向server发送消息
    4. server回应client
    5. 一次读写完成,连接不关闭
    6. 后续读写操作
    7. 长时间操作之后,client发起关闭请求
  • 长连接和短连接的对比
    • 长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间,对于频繁请求资源的客户来说,较适用长连接
    • client和server之间的连接如果一直不关闭的话,会存在一个问题:随着客户端连接越来越多,server早晚有扛不住的时候,这时候server需要采取一些策略,比如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损。如果条件再允许可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样就可以完全避免某个客户端连累后端服务
    • 短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段,但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽
  • 长连接和短连接的应用场景

    • 长连接多用于操作频繁、点对点的通讯,而且连接数不能太多的情况,比如数据库的连接。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,再次处理时直接发送数据包就可以了,不用建立TCP连接,而且频繁的socket创建也是对资源的浪费
    • 而像web网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,每个用户都占用一个连接的话,并发量会非常大

      通信结构图

      9. 网络编程 - 图7

      编程说明

  • tcp服务器需要绑定,否则客户端找不到这个服务器

  • tcp客户端不需要绑定,因为是主动连接服务器,所以只要确定好服务器的ip、port等信息就好
  • tcp服务器通过listen可以将socket创建出来的主动套接字变为被动套接字
  • 当客户端需要连接服务器时,就需要使用connect进行连接,udp是不需要连接而是直接发送,但是tcp必须先连接,只有连接成功才能通信
  • 当一个tcp客户端连接服务器时,服务器端会有一个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
  • listen后的套接字是被动套接字,用来接收新的客户端的连接请求,而accept返回的新套接字是标记这个新客户端的
  • 关闭listen后的套接字意味着被动套接字关闭了,会导致新的客户端不能够连接服务器,但是之前已经连接成功的客户端正常通信
  • 关闭accept返回的套接字意味着这个客户端已经服务完毕
  • 当客户端的套接字调用close后,服务器端会解阻塞,并且返回的长度为0,因此服务器可以通过返回数据的长度来判断客户端是否已经下线

    举例

    ```python import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_addr = (‘127.0.0.1’, 2000) client_socket.connect(server_addr) # 连接服务器

client_socket.send(b’hello, world, I am ZYJ’) # 发送数据

recv_data = client_socket.recv(100) # 接收数据 print(recv_data)

client_socket.close()

```python
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)

server_socket.listen(10)  # server_socket对象的缓冲大小

# 如果有新的客户端来连接服务器,那么就产生一个新的套接字专门为这个客户端服务
client_socket, client_addr = server_socket.accept()

recv_data = client_socket.recv(5)
print(recv_data)
recv_data = client_socket.recv(10)
print(recv_data)

client_socket.send(b'study')

client_socket.close()  # 关闭为这个客户端服务的套接字
server_socket.close()  # 关闭服务器

非阻塞编程

正常的recv、accept会阻塞,可以通过socket.setblocking(False)实现非阻塞编程

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
client_socket.connect(server_addr)  # 连接服务器

client_socket.send(b'hello, I am ZYJ')  # 发送数据

client_socket.close()
import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 重用对应地址和端口
server_socket.setblocking(False)  # 设置server_socket为非阻塞

server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(100)

client_socket = None

while True:
    try:
        client_socket, client_addr = server_socket.accept()
    except Exception as e:
        pass
    if client_socket:
        client_socket.setblocking(False)  # 设置client_socket为非阻塞
        try:
            recv_data = client_socket.recv(1024)
            if not recv_data:  # 如果客户端断开
                print('bye')
                exit()
            print(recv_data.decode('utf-8'))
        except Exception as e:
            pass

多路复用编程:epoll使用

epoll是对select、poll模型的改进,提高了网络编程的性能,广泛应用于大规模并发请求的C/S架构

相关用法

  • import select:导入select模块
  • epoll = select.epoll():创建一个epoll对象
  • epoll.register(文件句柄,事件类型):注册要监控的文件句柄和事件
  • 事件类型
    • select.EPOLLIN:可读事件
    • select.EPOLLOUT:可写事件
    • select.EPOLLERR:错误事件
    • select.EPOLLHUP:客户端断开事件
  • epoll.unregister(文件句柄):销毁文件句柄
  • epoll.poll(timeout):当文件句柄发生变化,则会以列表的形式主动报告给用户进程,timeout为超时时间,默认为 -1,即一直等待直到文件句柄发生变化。如果指定为1,那么epoll每一秒汇报一次当前文件句柄的变化情况,如果无变化则返回空
  • epoll.fileno():返回epoll的控制文件描述符
  • epoll.modify(fineno, envent):fineno为文件描述符,event为文件类型,该语句的作用是修改文件描述符所对应的事件
  • epoll.fromfd(fileno):从一个指定的文件描述符创建一个epoll对象
  • epoll.close():关闭epoll对象的控制文件描述符

    双方即时聊天

    ```python import socket import select import sys

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_addr = (‘127.0.0.1’, 2000) client_socket.connect(server_addr)

epoll = select.epoll() # 创建一个epoll对象 epoll.register(sys.stdin.fileno(), select.EPOLLIN) # 监控标准输入缓冲区 epoll.register(client_socket.fileno(), select.EPOLLIN) # 监控客户端的接收缓冲区

while True: epoll_events = epoll.poll(-1, 2) # -1表示永久等待,2表示监控了两个描述符 for fd, event in epoll_events: if fd == sys.stdin.fileno(): # 标准输入缓冲区里边有数据 send_data = input() client_socket.send(send_data.encode(‘utf8’)) elif fd == client_socket.fileno(): # 客户端的接收缓冲区里边有数据 recv_data = client_socket.recv(100) if not recv_data: print(‘服务器断开了’) exit() print(recv_data.decode(‘utf8’))

client_socket.close()

```python
import socket
import select
import sys

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)

client_socket, client_addr = server_socket.accept()

epoll = select.epoll()  # 创建一个epoll对象
epoll.register(sys.stdin.fileno(), select.EPOLLIN)  # 监控标准输入缓冲区
epoll.register(client_socket.fileno(), select.EPOLLIN)  # 监控接收客户端的缓冲区

while True:
    events = epoll.poll(-1, 2)  # -1表示永久等待,2表示监控了两个描述符
    for fd, event in events:
        if fd == sys.stdin.fileno():  # 标准输入缓冲区里边有数据
            data = input()
            client_socket.send(data.encode('utf8'))
        elif fd == client_socket.fileno():  # 接收客户端的缓冲区里边有数据
            recv_data = client_socket.recv(100)
            if not recv_data:
                print('客户端断开了')
                exit()
            print(recv_data.decode('utf8'))

client_socket.close()
server_socket.close()

客户端断开重连

客户端断开时,内核把socket对象对应的描述符标记为可读状态,epoll就会检测到,这时候服务器读到的内容为空,可以通过这个来判断,从而停止监测客户端

import socket
import select
import sys

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用对应地址和端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)

epoll = select.epoll()  # 创建一个epoll对象
epoll.register(sys.stdin.fileno(), select.EPOLLIN)  # 监控标准输入缓冲区
epoll.register(server_socket.fileno(), select.EPOLLIN)  # 监控服务器端的接收缓冲区 

client_socket = None
while True:
    epoll_events = epoll.poll(-1, 3)
    for fd, event in epoll_events:
        if fd == server_socket.fileno():  # 服务器被客户端连接了
            print('客户端连接了')
            client_socket, _ = server_socket.accept()
            epoll.register(client_socket.fileno(), select.EPOLLIN)  # 监控接收客户端的缓冲区
        elif fd == sys.stdin.fileno():  # 标准输入缓冲区里边有数据
            send_data = input()
            if not client_socket:
                print('客户端尚未连接')
                continue
            client_socket.send(send_data.encode('utf8'))
        elif fd == client_socket.fileno():  # 接收客户端的缓冲区里边有数据
            recv_data = client_socket.recv(100)
            if not recv_data:
                print('客户端断开了')
                epoll.unregister(client_socket.fileno())
                client_socket.close()
                continue
            print(recv_data.decode('utf8'))

client_socket.close()
server_socket.close()

多人聊天室

import socket
import select
import sys

if len(sys.argv) != 2:
    print('input as the format: python3 char_room_client.py IP')
    exit(2)

name = input("please input your name: ")

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = (sys.argv[1], 2000)
client_socket.connect(server_addr)

epoll = select.epoll()  # 创建一个epoll对象
epoll.register(client_socket.fileno(), select.EPOLLIN)  # 监控标准输入缓冲区
epoll.register(sys.stdin.fileno(), select.EPOLLIN)  # 监控客户端的接收缓冲区

while True:
    epoll_events = epoll.poll()
    for fd, event in epoll_events:
        if fd == sys.stdin.fileno():  # 标准输入缓冲区里边有数据
            try:  # Ctrl+D退出
                send_data = input()
            except:
                print('I will leave')
                client_socket.close()
                exit(2)
            send_data = name + ":" + send_data
            client_socket.send(send_data.encode('utf-8'))
        if fd == client_socket.fileno():  # 客户端的接收缓冲区里边有数据
            recv_data = client_socket.recv(100)
            if not recv_data:
                exit(0)
            print(recv_data.decode('utf-8'))

client_socket.close()
import socket
import select
import sys

if len(sys.argv) != 2:
    print('input as the format: python3 char_room_server.py IP')
    exit(2)

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 重用对应地址和端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

server_addr = (sys.argv[1], 2000)
server_socket.bind(server_addr)
server_socket.listen(10)

epoll = select.epoll()  # 创建一个epoll对象
epoll.register(server_socket.fileno(), select.EPOLLIN)  # 监控服务器端的接收缓冲区

client_dict = {}
client_socket = None

while True:
    epoll_events = epoll.poll()
    for fd, event in epoll_events:
        if fd == server_socket.fileno():  # 服务器被该客户端连接了
            client_socket, client_addr = server_socket.accept()
            print("{} is coming".format(client_addr))
            epoll.register(client_socket.fileno(), select.EPOLLIN)  # 监控接收该客户端的缓冲区
            client_dict[client_socket.fileno()] = client_socket
        else:  # 服务器的对应该客户端的接收缓冲区接收到数据
            recv_data = client_dict[fd].recv(10)
            if recv_data:
                for fileno, client in client_dict.items():
                    if fd != fileno:
                        client.send(recv_data)
            else:  # 对应该客户端的接收缓冲区里接收到的数据为空,说明该客户端断开了
                print('{} is leaving'.format(client_dict[fd]))
                epoll.unregister(fd)
                client_dict[fd].close()
                client_dict.pop(fd)

client_socket.close()
server_socket.close()

持续发送多个文件

方法:发送文件名的长度 —-> 发送文件名 —-> 发送文件内容的长度 —-> 发送文件内容

import socket
import struct

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_addr = ('127.0.0.1', 2000)
client_socket.connect(server_addr)

file_name_len = client_socket.recv(4)  # 接收文件名的长度
file_name = client_socket.recv(struct.unpack('I', file_name_len)[0])  # 接收文件名

file = open(file_name.decode('utf8'), 'wb')
file_content_len = client_socket.recv(4)  # 接收文件内容的长度
file_content = client_socket.recv(struct.unpack('I', file_content_len)[0])  # 接收文件内容
file.write(file_content)  # 将文件内容写入文件

file.close()
client_socket.close()
import socket
import struct

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 重用对应地址和端口

server_addr = ('127.0.0.1', 2000)
server_socket.bind(server_addr)
server_socket.listen(10)

client_socket, client_addr = server_socket.accept()

file_name = 'Readme'
file_name_bytes = file_name.encode('utf8')
client_socket.send(struct.pack('I', len(file_name_bytes)))  # 发送文件名的长度
client_socket.send(file_name_bytes)  # 发送文件名

file = open(file_name, 'rb')
file_content_bytes = file.read()
client_socket.send(struct.pack('I', len(file_content_bytes)))  # 发送文件内容的长度
client_socket.send(file_content_bytes)  # 发送文件内容

file.close()
client_socket.close()
server_socket.close()