python socket编程

Posted by

常用socket内容

获取主机名

import socket
hostname = socket.gethostname() # 获取主机名

根据主机名获取IP地址

import socket
hostname = socket.gethostname()
HOST = socket.gethostbyname(hostname) # 根据主机名获取IP地址

根据端口返回对应使用的应用程序

返回的是默认常用的,比如22端口对应ssh,443端口对应https

import socket
name = socket.getservbyport(443)
print(name)
# 输出的结果为 https

将网卡设置为混杂模式

import socket
s = socket.socket()
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

关闭混杂模式

import socket
s = socket.socket()
s.ioctl(socket.SIO_RCVALL,  socket.RCVALL_OFF)

开启UDP广播功能

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# 或者把1改为True:
# s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, True)

开启TCP长连接(keep-alive)

import socket
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True)
s.ioctl(socket.SIO_KEEPALIVE_VALS, (
    1,            # 开启保活机制
    60*1000,      # 1分钟后如果对方还没有反应,开始探测连接是否存在
    30*1000       # 30秒探测一次,默认探测10次,失败则断开
))

TCP

服务端_TCP

操作流程_TCP
  • 创建socket
  • 绑定ip和端口
  • 监听
  • 接受连接
  • 接受数据
  • 发送数据
常用循环流程_TCP
  • 创建socket.socket()对象
  • 绑定ip和端口
  • 监听
  • while 死循环
    • 接受连接
    • 接受数据
    • 发送数据
    • 关闭连接
      调用socket模块
      import socket
创建socket.socket()对象_TCP
tcpServerSocket = socket.socket()
  • socket.socket()默认创建的是ipv4的TCP套接字
    • socket.socket()括号中可以选择填写两个参数:
    • 第一个参数决定是ipv4还是ipv6:
      • socket.AF_INET:ipv4
      • socket.AF_INET6:ipv6
    • 第二个参数决定是TCP还是UDP:
      • socket.SOCK_STREAM:TCP
      • socket.SOCK_DGRAM:UDP
绑定ip和端口_TCP
host = ''
port = 80
addr = (host,port)
tcpServerSocket.bind(addr)
  • bind()方法括号中填写的是一个元组
    • 这个元组第一个元素应该是一个字符串,用来代表绑定的ip地址
    • ip地址如果为''(也就是空字符串),就代表监听0.0.0.0
    • ip地址如果为'127.0.0.1',就代表监听本地,局域网和外网的连接都不接受
    • ip地址也可以为网卡显示的地址,代表监听的是0.0.0.0
    • 这个元组第二个元素应该是一个整型数值,用来代表绑定的端口
监听_TCP
tcpServerSocket.listen()
# tcpServerSocket.listen(128)
  • listen()方法代表监听,括号中可以有参数,参数应该是一个整型数值,这个整型数值代表两次socket连接中最多可以有多少次连接可以被缓存(上一个socket连接结束后从缓存中按顺序接受下一次的连接)
接受连接_TCP
tcpClientSocket,addr = tcpServerSocket.accept()
  • accept()方法返回的是一个元组
    • 这个元组的第一个元素是socket.socket类型的数据,接下来的连接、接受和发送数据用到的会是这个元素,而不再是前面创建的tcpServerSocket
    • 这个元组的第二个元素是一个元组,代表客户端的连接地址和端口
接收数据_TCP
data = tcpClientSocket.recv(1024)
  • 接受到的数据类型为二进制(byte)
  • recv()方法用于接受数据,recv()括号中必须要填写一个整型数值,这个整型数值代表最多接收多少字节的数据,如果我们想要接收任意大的数据,我们可以采用以下的方式:
    fullDataBytes = b''
    while True:
    data = tcpClientSocket.recv(1024)
    fullDataBytes += data
    if len(data) < 1024:
        break
发送数据_TCP
s = b'HTTP/1.1 200 OK\r\n'
tcpClientSocket.send(s)
  • send()方法用于数据发送,发送的数据类型只能为二进制(byte)

如果我们想要发送任意大的数据,我们可以选用sendall()

fp = open('abc.jpg','rb')
s = fp.read()
tcpClientSocket.sendall(s)
关闭连接_TCP
tcpClientSocket.close()
举例_TCP
import socket
host = ''
port = 8080
addr = (host,addr)
tcpServerSocket = socket.socket()
tcpServerSocket.bind(addr)
tcpServerSocket.listen(128)
while True:
    print('等待连接...')
    tcpClientSocket, addr = tcpServerSocket.accept()
    print('连接成功,地址为:',addr)
    fullDataBytes = b''
    while True:
        data = tcpClientSocket.recv(1024)
        fullDataBytes += data
        if len(data) < 1024:
            break
    print('接收到的数据为:',data)
    print('准备发送数据')
    m = b'HTTP/1.1 200 OK\r\nServer: abc\r\nContent-Type: image/jpeg\r\n\r\n'
    tcpClientSocket.sendall(m)
    print('数据发送成功')
    tcpClientSocket.close()
    print('连接关闭')

tcpServerSocket.close()

客户端_TCP

  • 客户端不需要绑定(bind()),不需要监听(listen()),不需要等待连接(accept()),客户端需要主动建立连接(connect())
  • connect()用法和bind()相同,括号中为一个元组,元组第一个元素为host,第二个元素为port
举例-客户端-TCP
import socket
Client = socket.socket()
Client.connect(('127.0.0.1',8080))
m = b'Hello'
Client.send(m)
n = Client.recv(2048)
print(n)

TCP断包与粘包

UDP

服务端_UDP

操作流程_UDP
  • 创建socket.socket()
  • 绑定ip和端口
  • 接收数据
  • 发送数据
常用循环流程_UDP
  • 创建socket.socket()
  • 绑定ip和端口
  • while True:
    • 接收数据
    • 发送数据
调用socket模块_UDP
import socket
创建socket.socket()对象_UDP
s = socket.socket()
绑定ip和端口_UDP
host = ''
port = 456
addr = (host, port)
s.bind(addr)
接收数据_UDP
  • 与TCP不同的是,接收数据使用的不是recv()方法,而是recvfrom()方法
  • s.recvfrom(bufsize)返回的是一个元组
    • 元组第一个元素为data,也就是接收到的二进制数据
    • 这个元组的第二个元素是一个元组:
    • 第一个元素为host,也就是客户端的ip地址
    • 第二个元素为port,也就是客户端的端口
发送数据_UDP
  • 与TCP不同的是,发送数据使用的不是send()方法,而是sendto()方法
  • s.sendto(data, address):
    • data为要发送的二进制数据
    • address为一个元组(host, port):
    • host为客户端的ip地址
    • port为客户端的端口,这里我们一般直接使用recvfrom()方法接收到的address元组
举例_UDP
import socket
host = ''
port = 456
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((host,port))
while True:
    data, addr = s.recv(2048)
    print(f'从{addr}发送过来的数据为:', data)
    s.sendto(b'Hello!', addr)
s.close()

客户端_UDP

  • 客户端不需要绑定(bind()),因为是UDP传输,不需要提前和服务器建立连接,也就是不需要(connect())
举例-客户端-UDP
import socket
host = '127.0.0.1'
port = 456
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data = b'Hello!'
addr = (host, port)
c.sendto(c, addr)
data_recv, addr_recv = c.recvfrom(2048)
print(f'从服务器{addr_recv}接收到的数据为:',data_recv)
c.close()

常用实例

网络嗅探器的简单实现

原理_网络嗅探器
  • 一般接收数据时,都会先验证Mac和IP,确保这个数据是发给本机的而不是其他主机的,而我们可以用python将网卡的数据接收调为混杂模式,也就是所有数据全部都接收,同时,socket监听的端口应该设置为0,也就是任意端口(不分配固定端口号,由系统自动分配)
  • 因为是要接收所有的数据,肯定同时包括了TCP和UDP的数据,因此我们应该要建立原始套接字,而不是单纯建立TCP套接字或者UDP套接字,原始套接字需要用到socket.SOCK_RAW,如果我们要监听ipv4的地址,配置也就是:s = socket.socket(socket.AF_INET, socket.SOCK_RAW)
  • 需要注意的一点是,如果我们要将网卡设置为混杂模式,我们需要以管理员的身份运行这个程序
  • 因为网络嗅探的过程中,我们接收的是任意ip发过来的内容,这点类似于UDP的传输而不是TCP的传输,因此我们在接收数据时应该使用recvfrom()方法而不是recv()方法,并且使用recvfrom()方法的另一个优点是它会给我们返回发送数据的主机ip地址和端口
实现_网络嗅探器

注意:s.ioctl()应该放在s.bind()后面,否则会报错,并且s.bind()不能绑定所有网卡,也就是不能为s.bind(('',0)),应该给一个具体的ip地址,可以是本机127.0.0.1(这样只会监听本机的数据包),如果想要监听局域网内的所有数据包,我们应该绑定的是本机局域网的ip地址,比如192.168.1.123,局域网的ip地址可以用socket.gethostbyname(socket.gethostname())获得

import socket

# 获取本机ip(局域网)
HOST = socket.gethostbyname(socket.gethostname())

# 建立socket.socket()对象(ipv4[AF_INET]和原始套接字[SOCK_RAW])
s = socket.socket(AF_INET, SOCK_RAW)

# 绑定ip和端口
s.bind((HOST,0))

# 接收所有数据包(网卡 -> 混杂模式)
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

while True:
    data, addr = s.recvfrom(20480)
    host = addr[0]
    print(data,host)

# 关闭混杂模式
s.ioctl(socket.SIO_RCVALL, RCVALL_OFF)

s.close()

端口扫描器的原理与实现

原理_端口扫描器

如果一台服务器的一个端口开放了TCP连接,那么我们就可以使用connect()来尝试连接服务器,如果连接成功,就代表相应端口开放,然后我们可以根据开放的端口来猜测服务器上可能使用的程序或服务

实现_端口扫描器
import socket
for port in range(76,100): # 只扫描端口76~99
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect(('lcj.ink',port))
        print(f'端口{port}开放,对应的默认服务为{socket.getservbyport(port)}')
        s.close()
    except socket.error:
        print(f'端口{port}未开放')
  • 需要注意的是,socket.getservbyport()如果没找到对应端口的默认常用服务,会报错,因此我们可以不实用这个
  • 这个程序运行起来会比较慢,因为没连接上程序不会立马判断,而是会等待一段时间,如果超时服务器还没响应,才会判断没有连接上,解决方法有两种,一种是设置系统的TCP连接超时时间,另一种是使用多进程

UDP广播

原理_UDP广播
  • 服务端

    • 使用sendto(data,address)发送数据时,address的ip地址使用广播ip,服务端则会在响应ip段内进行广播,比如:ip为192.168.1.255,那么广播的范围就为192.168.1.1~192.168.1.255;ip为10.255.255.255,那么广播范围就为10.*.*.*
  • 客户端

    • 正常接收数据,需要和服务端有一个暗号,服务端发送这个暗号并被客户端接收后,客户端才能认为这个消息来自服务端的广播消息,因为UDP可以接收来自所有ip的消息,如果没有这个暗号,我们不能确定这个消息来自广播服务器

需要注意的一点,这里的服务端使用的是connect(),客户端才需要绑定ip和端口bind(),因为如果使用connect(),只支持先发数据后接收数据,如果先接收数据,程序会报错

实现_UDP广播

服务端:

import socket
import time
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) # 如果没有这一行,那么我们就没有相应权限
# 获取本机ip
host = socket.gethostbyname(socket.gethostname())

# 将host变为广播ip
host = host.split('.')
host[-1] = '255'
host = '.'.join(host)

data = '暗号'.encode()
while True:
    # s.sendto(data,(host, 456)) 这么设置无法广播,原因暂时不清楚,我们先将广播ip设置为255.255.255.255,这样局域网都可以收到广播
    s.sendto(data, ('255.255.255.255', 456))
    time.sleep(2)

客户端:

import socket
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.bind(('',456))
while True:
    data, addr = c.recvfrom(1024)
    data = data.decode()
    if data == '暗号':
        print(f'收到来自{addr}的广播消息')

TCP的断包与粘包

原理_TCP断包与粘包
实现_TCP断包与粘包

TCP长连接保持存活(keep-alive)

原理_keepalive

双方建立TCP连接后,由服务端或者客户端一方向对方每隔一定时间发送一个心跳包,以此确定连接未中断,也能确保连接不会中断

实现_keepalive
import socket
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True)
s.ioctl(socket.SIO_KEEPALIVE_VALS, (1, 60*1000, 30*1000))
s.connect(('lcj.ink', 6666))
# ......