python语法之socket编程初探

引言

  • 什么是socket?

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

  • socket能干嘛?

    • 我们学习socket就是为了完成C/S架构的开发,那什么是C/S架构开发呢?传统意义上C/S架构分为硬件C/S架构软件C/S架构。在这里C/S架构开发一般指的是软件C/S架构。在互联网上到处都是C/S架构,如CSDN网站是服务端,你的浏览器是客户端(B/S架构也是C/S架构的一种),腾讯作为服务端为你提供视频,你得下个腾讯视频客户端才能看它的视频)。
  • 为什么学习socket需要懂得互联网协议?

    1. socket编程就是为了开发一款软件C/S架构
    2. C/S架构的软件(软件属于应用层)是基于网络进行通信的
    3. 网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准。
    4. 附上一张图
      在这里插入图片描述
  • 在上图中,我们没有看到Socket的影子,那么它到底在哪里呢?请看下图:
    在这里插入图片描述

  • socket发展史及分类

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。
因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同
一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

  1. 基于文件类型的套接字家族

    套接字家族的名字:AF_UNIX,unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信。

  2. 基于网络类型的套接字家族

    套接字家族的名字:AF_INET,还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET。


socket之Python实现

  • 既然是C/S架构,那么socket编程就需要实现两部分,一个是服务端,另一个是客户端。先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束(注意,就是指的是基于TCP的套接字,UDP协议的套接字更简单一些。)可以参考下图:

  • 具体到python中代码实现socket编程,主要是通过 socket 这个模块,这里做一个简单介绍:

import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是
SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。

获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

  1. 服务端套接字函数

    s.bind() 绑定(主机,端口号)到套接字
    s.listen() 开始TCP监听
    s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

  2. 客户端套接字函数
    s.connect() 主动初始化TCP服务器连接
    s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

  3. 公共用途的套接字函数
    s.recv() 接收TCP数据
    s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
    s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
    s.recvfrom() 接收UDP数据
    s.sendto() 发送UDP数据
    s.close() 关闭套接字

  • 基于TCP套接字初始版:

服务端:

# author:dayin
import socket
# 第一个参数 表示 基于网络类型的套接字家族  第二个参数表示 基于流式的 可以简单理解为 基于TCP协议
ip_port = ('127.0.0.1', 8000)
back_log = 5
buffer_size=1024
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind(ip_port)
server.listen(back_log)
print('>>>>等待客户端连接...')
# 阻塞 等待 客户端连接
conn, address = server.accept()
print(address)
# 收到 1024个字节信息
message = conn.recv(buffer_size)
print('>>>>接收到客户端发来的数据:{}'.format(message.decode()))
# 发送数据给客户端
conn.send(message.upper())
conn.close()
server.close()

客户端:

# author:dayin
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size=1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)
message = input('>>>请输入发送给服务端消息:')
client.send(message.encode())
rev_msg = client.recv(buffer_size)
print('收到服务器消息:{}'.format(rev_msg.decode()))
client.close()

代码实现如下图:

启动TCP服务端:

可以看出,服务端程序,阻塞在 server.accept(),等待 着客户端连接。

启动TCP客户端:


可以看出,此时服务端程序,又阻塞在 conn.recv(buffer_size) 这里,等待着客户端向服务端发送数据。


当客户端发送数据给服务端后,服务端和客户端都终止运行了。此时,服务端收到客户端的数据"hello world",并将其转化为大写字符"HELLO WORLD"并传送给客户端,这样,就是实现了 服务端和客户端的一次通信。

But,这很显然不符合C/S架构,C/S架构需要实现的是,服务端一直运行下去,不能因为客户端的断开,而服务端也要断开连接。此外,服务端和客户端只进行一次通信,这也是不合理的。这时,就需要加入 链接循环(为了使得服务器一直运行下去,等待着客户端的连接。) 与 消息循环(为了实现客户端和服务端的多次通信)。

  • 基于TCP套接字进阶版:

服务端:

# author:dayin
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
backlog = 5
# 第一个参数 表示 基于网络类型的套接字家族  第二个参数表示 基于流式的 可以简单理解为 基于TCP协议
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.bind(ip_port)
# listen(5) 表示 最多可以有5个客户接入,这里的接入应指的是等待
server.listen(backlog)
# 链接循环
while True:
    print('>>>>等待客户端连接...')
    # 阻塞 等待 客户端连接
    conn, address = server.accept()
    print('客户端>>>', address, '连接>>>>')
    # 消息循环
    while True:
        message = conn.recv(buffer_size)
        if not message: break
        print('>>>>接收到客户端发来的数据:{}'.format(message.decode()))
        # 发送数据给 客户端
        conn.send(message.upper())
    conn.close()

客户端:

# author:dayin
# Date:2019/7/26 0026
import socket
ip_port = ('127.0.0.1', 8000)
buffer_size = 1024
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ip_port)
while True:
    message = input('>>>请输入发送给服务端消息(输入quit退出链接):')
    if message == 'quit': break
    client.send(message.encode())
    rev_msg = client.recv(buffer_size)
    print('收到服务器消息:{}'.format(rev_msg.decode()))
client.close()

结果如下图:

启动服务端:

启动客户端:


客户端正常退出:

服务端此时并未退出,而是继续监听,等待下一个客户端连接:

PS:

  1. 代码还有改进的地方,当客户端异常断开时,服务端将会抛出如下图的异常:(解决思路,在消息循环中加入 try except 捕捉异常即可)
  2. 未能实现并发,即当一个客户端和服务端连接上后,如果此时还有一个客户端连接到服务端,那么该客户端需要等待其它客户端断开连接后,才能够与服务端进行连接。解决思路:使用socketserver,我将在另一篇博客进行实现。
  3. 当客户端直接键入回车时,发现客户端和服务端都阻塞了,这是为什么呢?这里涉及到一些底层原理,这里我只简单解释一下,如需了解更深,请自行查询资料。客户端和服务端进行通信时,表面上似乎是客户端将信息传给服务端,内部的实现是不可见的。这里的内部实现,其实是客户端将需要传递的消息传递给缓存,缓存负责将消息传递给服务端的缓存,也就是说服务端获得消息是从服务端的缓存中获得的,So,当客户端向缓存发送时,客户端的缓存并未收到任何消息,一直在阻塞着,而服务端的缓存也未收到客户端的缓存,也一直阻塞着,这就导致客户端和服务端互相阻塞。解决思路:在客户端中加上一个判断即可
if not messge: continue
  • 基于UDP套接字的简单示例(聊天室)

    由于UDP是无连接的,所以可以同时多个客户端去跟服务端通信。也就不存在链接循环。

服务端:

# author:dayin
import socket
ip_port = ('192.168.1.2', 8080)
buffer_size = 1024
# UDP 是数据报格式
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # 数据报
# 绑定ip 和 端口
server.bind(ip_port)
print('等待客户端连接....')
# 服务端循环,等待客户端连接
while True:
    msg, addr = server.recvfrom(buffer_size)
    print('收到客户端 ip_port为{},收到消息为 {}'.format(addr, msg.decode()))
    # 发送消息给客户端
    server.sendto(msg.upper(), addr)

客户端:

# author:dayin
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ip_port = ('192.168.1.2', 8080)
buffer_size = 1024
while True:
    msg = input('>>>>').strip()
    # 发送消息给服务端
    client.sendto(msg.encode(), ip_port)
    # 接收服务端的消息
    data, addr = client.recvfrom(buffer_size)
    print('收到服务端的消息为:', data.decode())

结果如下图:

启动服务端:


启动两个客户端:

PS:

  1. udp是无链接的,先启动哪一端都不会报错。
  2. udp服务端可以接入多个客户端
  3. udp客户端只输入回车时,服务端和客户端并不会像TCP一样阻塞,这是因为udp发送消息时,都带有数据报头信息,这样即使数据报内容是空的,发送给客户端缓存时,并不是空的!所以,客户端的缓存能够将数据报发送给服务端的缓存,服务端程序就能接收到客户端的消息,此时的消息为空。

おすすめ

転載: blog.csdn.net/weixin_42218582/article/details/97390797