网络编程之:Socket套接字

  • Socket又称"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求.
  • socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

socket和file的区别:

  • file模块是针对某个指定文件进行【打开】【读写】【关闭】
  • socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】

1.1 语法格式

socket.socket([family[, type[, proto]]])
**family: **

  • socket.AF_INET(IPv4); (默认)
  • AF_INET6(IPv6);
  • AF_UNIX只能够用于单一的Unix系统进程间通信
    type:
  • 面向连接: SOCK_STREAM(for TCP)(默认)
  • 非连接分: SOCK_DGRAM(for UDP)
  • SOCK_RAW: 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
  • SOCK_RDM: 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
  • SOCK_SEQPACKET 可靠的连续数据包服务

protocol:

  • 0(默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议

1.2 服务器端套接字

  • ser.bind(): 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
  • ser.listen(): 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5就可以了。
  • ser.accept(): 被动接受TCP客户端连接,(阻塞式)等待连接的到来

1.3 客户端套接字

  • client.connect(): 主动初始化TCP服务器连接,。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。
  • client.connect_ex(): connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

1.4 公共用途的套接字函数

  • s.recv(): 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flag提供有关消息的其他信息,通常可以忽略。
  • s.send(): 发送TCP数据,将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。
  • s.sendall(): 完整发送TCP数据,完整发送TCP数据。将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
  • s.recvfrom(): 接收UDP数据,与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。
  • s.sendto(): 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。
  • s.close(): 关闭套接字
  • s.getpeername(): 返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。
  • s.getsockname(): 返回套接字自己的地址。通常是一个元组(ipaddr,port)
  • s.setsockopt(level,optname,value): 设置给定套接字选项的值。
  • s.getsockopt(level,optname[.buflen]): 返回套接字选项的值。
  • s.settimeout(timeout): 设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
  • s.gettimeout(): 返回当前超时期的值,单位是秒,如果没有设置超时期,则返回None。
  • s.fileno(): 返回套接字的文件描述符。
  • s.setblocking(flag): 如果flag为0,则将套接字设为非阻塞模式,否则将套接字设为阻塞模式(默认值)。非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常。
  • s.makefile(): 创建一个与该套接字相关连的文件

1.5 SocketServer

  • 4种类型:
class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)
class socketserver.UnixDatagramServer(server_address, RequestHandlerClass,bind_and_activate=True)

        +------------+
        | BaseServer |
        +------------+
              |
              v
        +-----------+        +------------------+
        | TCPServer |------->| UnixStreamServer |
        +-----------+        +------------------+
              |
              v
        +-----------+        +--------------------+
        | UDPServer |------->| UnixDatagramServer |
        +-----------+        +--------------------+

  • 创建一个socketserver 至少分以下几步:
First, you must create a request handler class by subclassing the BaseRequestHandlerclass and overriding its handle() method; this method will process incoming requests.   
Second, you must instantiate one of the server classes, passing it the server’s address and the request handler class.
Then call the handle_request() or serve_forever() method of the server object to process one or many requests.
Finally, call server_close() to close the socket.
  • 让你的socketserver并发起来, 必须选择使用以下一个多并发的类
1. 多进程
class socketserver.ForkingTCPServer
class socketserver.ForkingUDPServer

2. 多线程
class socketserver.ThreadingTCPServer
class socketserver.ThreadingUDPServer


将
server = socketserver.TCPServer((HOST, PORT), MyTCPHandler)
换成:
server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)

2 实例

2.1 服务端与客户端接收数据

  • 客户端
import socket
clinet = socket.socket()   # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969))  # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来

clinet.send(b"Hello World")
# byte类型只能支持ASCii码的数据类型
# 输出:TypeError: a bytes-like object is required, not 'str'  # 默认只支持byte类型
# 若想输入中文,应当使用encode进行编码成byte
clinet.send('你好'.encode('utf-8'))

data = clinet.recv(1024)
print('recv: ', data)
# 输出:recv:  b'\xe4\xbd\xa0\xe5\xa5\xbd'
# 此时需要显示中文,需要对编码后的数据进行解码
print('recv: ', data.decode('utf-8'))
# 输出:recv:  你好

clinet.close()
  • 服务端
import socket

ser = socket.socket()
ser.bind(('localhost', 6969))  # 绑定要监听的端口
ser.listen()  # 监听该端口

print('允许连接')
# ser.accept()  # 允许进入
conn, addr = ser.accept()  # conn 就是客户端连接过来而在服务器端所生成的一个实例
print(conn, addr)
# <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
print('准备接收')
data = conn.recv(1024)
print('recv: ', data)
conn.send(data.upper())

ser.close()

2.2 优化1:

  • 使客户端可以与服务端进行多次输入互动,但无法实现多客户端同时与服务端建立会话;

  • 当客户端断开时,服务端因为收到空的数据,会产生死循环(linux下有效);

  • 客户端

import socket

clinet = socket.socket()   # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969))  # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来
while True:   # 在第一次数据发送完毕后,再发送新数据
    msg = input('# ').strip()
    clinet.send(msg.encode('utf-8'))

    data = clinet.recv(1024)
    print('recv: ', data.decode('utf-8'))

clinet.close()
  • 服务端
import socket

ser = socket.socket()
ser.bind(('localhost', 6969))  # 绑定要监听的端口
ser.listen()  # 监听该端口

print('允许连接')
# ser.accept()  # 允许进入
conn, addr = ser.accept()  # conn 就是客户端连接过来而在服务器端所生成的一个实例
print(conn, addr)
# <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
while True:   # 在第一次接收到数据结束时,再重新开始接收新数据
    print('准备接收')
    data = conn.recv(1024)

    print('recv: ', data)
    conn.send(data.upper())

ser.close()

2.3 优化2:

  • 允许多客户端与服务端进行多次会话(不能同时), 当前会话断开时,再会与后续请求建立连接

  • 但会出现输入空字符卡死的情况。

  • 客户端

与上内容不变
  • 服务端
import socket

ser = socket.socket()
ser.bind(('localhost', 6969))  # 绑定要监听的端口
ser.listen()  # 监听该端口
while True:
    print('允许连接')
    # ser.accept()  # 允许进入
    conn, addr = ser.accept()  # conn 就是客户端连接过来而在服务器端所生成的一个实例
    print(conn, addr)
    # <socket.socket fd=592, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6969), raddr=('127.0.0.1', 5431)> ('127.0.0.1', 5431)
    while True:
        print('准备接收')
        data = conn.recv(1024)
        print('recv: ', data)
        if not data:  # 当接收数据为空,则退出循环
            break
        conn.send(data.upper())
ser.close()

2.4 优化2:

  • 客户端
import socket

clinet = socket.socket()   # 声明socket类型,同时生成socket连接对象
clinet.connect(('localhost', 6969))  # connect()只接收一个参数,但我们的连接包含IP地址与端口,需要用括号括起来
while True:
    msg = input('# ').strip()
    if len(msg) == 0:continue  # 注意:socket发不了空,发不了空,发不了空
    clinet.send(msg.encode('utf-8'))
    data = clinet.recv(1024)
    print('recv: ', data.decode('utf-8'))
clinet.close()
  • 服务端
与上内容不变

2.5 模拟ssh客户端

  • 客户端
import socket

clinet = socket.socket()
clinet.connect(('localhost', 8888))

while True:
    msg = input('# ').strip()
    if len(msg) == 0:continue
    clinet.send(msg.encode('utf-8'))
    cmd_res_size = clinet.recv(1024)
    clinet.send('ready to recive'.encode('utf-8'))  # 客户端回包确认
    print(cmd_res_size)
    received_size = 0
    received_data = b''
    while received_size < int(cmd_res_size.decode('utf-8')):
        data = clinet.recv(1024)
        received_size += len(data)  # 每次收到的有可能是小于1024字节,所以必须用len判断
        received_data += data
    else:
        print('cmd res has received done.', received_size)
        print(received_data.decode('utf-8'))

clinet.close()

  • 服务端
import os
import socket
import time

server = socket.socket()
server.bind(('localhost', 8888))
server.listen()

while True:
    conn, addr = server.accept()
    while True:
        data = conn.recv(1024)
        print(data)
        if not data:
            break
        res = os.popen(data.decode('utf-8')).read()
        if len(res) == 0:
            res = 'cmd has no output...'
        conn.send(str(len(res)).encode('utf-8'))  # 先发大小给客户端,解决无法将数据一次性返回问题
        # time.sleep(0.5)
        client_ack = conn.recv(1024)  # 等待客户端返回,解决socket粘包问题,否则会在linux下可能会报错
        conn.send(res.encode('utf-8'))

server.close()


# 输出:
# python3.8 socket_client.py  
# ls
recv:  socket_client.py
socket_ser.py

# df -h
recv:  Filesystem      Size  Used Avail Use% Mounted on
udev            959M     0  959M   0% /dev
tmpfs           199M  1.6M  198M   1% /run
/dev/sda1        77G   35G   39G  48% /
tmpfs           993M     0  993M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           993M     0  993M   0% /sys/fs/cgroup
tmpfs           199M   12K  199M   1% /run/user/131
vmhgfs-fuse     216G   41G  176G  19% /mnt/hgfs
tmpfs           199M   20K  199M   1% /run/user/0

2.6 模拟ftp服务

服务端

import os
import socket
import hashlib

server = socket.socket()
server.bind(('localhost', 2121))
server.listen()

while True:
    conn, addr = server.accept()
    while True:
        data = conn.recv(1024)
        if not data:
            print('Client is closed')
            break
        cmd, filename = data.decode().split()
        print(filename)
        if os.path.isfile(filename):
            file_size = os.stat(filename).st_size
            conn.send(str(file_size).encode())
            conn.recv(1024)
            md5_hash = hashlib.md5()
            with open(filename, 'rb') as f:
                for line in f:
                    conn.send(line)
                    md5_hash.update(line)
            conn.send(filename + ' md5 is ' + md5_hash.hexdigest())
        else:
            print('There is no that file')

        print('send done')

server.close()


客户端


import socket
import hashlib

md5_hash = hashlib.md5()

client = socket.socket()
client.connect(('192.168.10.11', 2121))

while True:
    cmd = input('# ').strip()
    if len(cmd) ==0:continue
    if cmd.startswith('get'):
        client.send(cmd.encode())
        rec_file_size = client.recv(1024)
        print('the file size is ', rec_file_size)
        client.send('read'.encode())
        received_size = 0
        filename = cmd.split()[1]
        with open(filename, 'wb') as f:
            while received_size < int(rec_file_size.decode()):
                if int(rec_file_size) - received_size > 1024:  # 解决粘包问题
                    size = 1024
                else:
                    size = int(rec_file_size) - received_size
                data = client.recv(size)
                received_size += len(data)
                f.write(data)
                md5_hash.update(data)
                print(rec_file_size, received_size)
            else:
                received_md5 = client.recv(1024)
                print('received_md5', received_md5)
                print('flie received done. and ' + filename + ' md5 is ' + md5_hash.hexdigest())
client.close()



2.7 socketserver

socketserver_server

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):

    def handle(self):

        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote".format(self.client_address[0]))
                print(self.data)
                self.request.sendall(self.data.upper())
                if not self.data:
                    print(self.client_address[0], "closed!")
                    break
            except ConnectionResetError as error:
                print("Error", error)
                break


if __name__ == '__main__':
    HOST, PORT = "localhost", 9999

    server = socketserver.ThreadingTCPServer((HOST, PORT), MyTCPHandler)
    server.serve_forever()


socketserver_client

import socket

client = socket.socket()
client.connect(("localhost", 9999))

while True:
    msg = input(">>:").strip()
    if len(msg) == 0: continue
    client.send(msg.encode("utf-8"))
    data = client.recv(10240)
    print("recv:", data.decode())

client.close()

posted @ 2020-11-07 06:17  f_carey  阅读(19)  评论(0编辑  收藏  举报  来源