浅析python-socket编程

1. 什么是socket?

  socket是套接字的英文名称, 我们知道在TCP/IP协议簇体系中,将网络状态分为了应用层、传输层、网络层、物理层等四种状态,而socket是与传输层密切相关的,其主要实现协议为TCP及UDP。传输层实现端到端的通信,因此,每一个传输层连接有两个端点。那么,传输层连接的端点是什么呢?不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口。传输层连接的端点叫做套接字(socket)。根据RFC793的定义:端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16位的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)。
总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的一个端点。通信时,其中的一个网络应用程序将要传输的一段信息写入它所在主机的Socket中,该Socket通过网络接口卡的传输介质将这段信息发送给另一台主机的Socket中,使这段信息能传送到其他程序中。因此,两个应用程序之间的数据传输要通过套接字来完成。
在网络应用程序设计时,由于TCP/IP的核心内容被封装在操作系统中,如果应用程序要使用TCP/IP,可以通过系统提供的TCP/IP的编程接口来实现。在Windows环境下,网络应用程序编程接口称作Windows Socket。为了支持用户开发面向应用的通信程序,大部分系统都提供了一组基于TCP或者UDP的应用程序编程接口(API),该接口通常以一组函数的形式出现,也称为套接字(Socket)。
 

1. socket编程基本流程图

     

 2. socket、threading实现聊天通信 

# coding:utf-8
# 服务端:

import socket import threading HOST, PORT
= "localhost", 8020 address = (HOST, PORT) # socket.AF_INET代表IPV4, socket.AF_INET6代表IPV6 # socket.SOCK_STREAM代表TCP, SOCK_DGRAM代表UDP server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(address) server.listen() def handlesock(sock, addr): while True: data = sock.recv(1024).decode("utf-8") if data == "exit": break print("收到来自客户端[{0}]的消息, 内容是[{1}]".format(addr, data)) response = input("回复[{0}]: ".format(addr)).encode("utf8") sock.send(response) while True: sock, addr = server.accept() cur_sock = threading.Thread(target=handlesock, args=(sock, addr)) cur_sock.start()
# 客户端:
import socket client
= socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(("localhost", 8020)) while True: response = input("回复服务器:").encode("utf8") client.send(response) if response=="exit": break data = client.recv(1024).decode("utf8") print("收到来自服务器的消息:%s" % data) client.close()

  解释:对于单用户、单服务来说,一个服务器只服务一个用户对象, 那么要想实现两端通信, 能够通过循环来实现输入与输出;若是服务于多个用户对象, 就需要保留每一个连接(socket实例)对象来单独处理,这里我们采用了线程的方式,即每接收一个连接请求,就新开辟一个线程来处理(考虑到内存开销也可以用线程池来替代)。

3. socket模拟浏览器发送http请求

  在此之前,我们必须弄懂相关概念,

  ###:socket并不是一种协议,而是对TCP/IP协议(或UDP/IP协议)的高级封装,其与TCP连接是可以存在包含关系的,换一句话说,socket就是一种通信接口,拥有实现可靠连接的功能。此外,当socket(网络套接字)采用TCP来建立连接的时候,是面向长连接的。

  ###:http连接显著的特点是客户端发送的每次请求都需要服务器回送相应响应,在请求结束后,会主动释放连接,从建立连接到关闭连接的过程称之为“一次连接”。此外HTTP连接是无状态的,所以在请求过程中需要发送请求头来标明请求身份。

  (HTTP请求底层是一种特殊处理后的socket,是面向短连接的)

#coding:utf-8

from urllib.parse import urlparse
import socket

class SocketHttp():
    """模拟http请求"""
    def __init__(self, url: str):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.url = url

    def parse_url(self):
        """url解析"""
        self.url = urlparse(self.url)
        domain = self.url.netloc
        path = self.url.path
        if path == "":
            path = "/"
        return domain, path

    def request(self):
        """建立socket连接, 请求url"""
        domain, path = self.parse_url()
        self.client.connect((domain, 80))
        request_data = """GET {0} HTTP/1.1
Host: {1}
Connection: close

""".format(path, domain).encode("utf8")
        self.client.send(request_data)
        # 读取数据
        response = b""
        while True:
            cur_data = self.client.recv(1024)
            if cur_data:
                response+=cur_data
            else:
                break
        self.client.close()
        return response.decode()


if __name__ == '__main__':
    client_http = SocketHttp("https://www.baidu.com/")
    print(client_http.request())

  输出:

  

  我们也可以采用异步非阻塞IO来获取http响应

#coding:utf-8

from urllib.parse import urlparse
import socket

class SocketHttp():
    """模拟http请求"""
    def __init__(self, url: str):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
        self.url = url

    def parse_url(self):
        """url解析"""
        self.url = urlparse(self.url)
        domain = self.url.netloc
        path = self.url.path
        if path == "":
            path = "/"
        return domain, path

    def request(self):
        """建立socket连接, 请求url"""
        domain, path = self.parse_url()
        try:
            self.client.connect((domain, 80))
        except BlockingIOError:
            pass
        request_data = """GET {0} HTTP/1.1
Host: {1}
Connection: close

""".format(path, domain).encode("utf8")

        # 轮询尝试发送,直到与服务器连接成功
        while 1:
            try:
                self.client.send(request_data)
            except OSError as e:
                pass
            else:
                break

        # 读取数据
        response = b""
        while True:

            # 不断轮询接收数据
            while 1:
                try:
                    cur_data = self.client.recv(1024)
                except BlockingIOError as e:
                    pass
                else:
                    break

            if cur_data:
                response+=cur_data
            else:
                break
        self.client.close()
        return response.decode()


if __name__ == '__main__':
    client_http = SocketHttp("https://www.baidu.com/")
    print(client_http.request())
异步非阻塞IO

4.非租塞IO

   当两个通信实体成功建立连接之后,recv方法调用之后,其内核空间与应用空间的发生的数据复制行为如下

  

  可以看出,若调用recv方法之后,数据未准备好, socket所处线程一直处于阻塞状态,直至数据复制到用户空间。这时,CPU未得到有效利用。那么如何才能利用起cpu资源呢?轮询方式便产生了(非阻塞I/O模式)

  当线程执行recv方法时,不再让该线程睡眠,而是立即返回一个错误状态,以此不断轮询直到返回正确状态才退出循环,值得注意的是该轮询的方式需要我们手动实现。如下:

  

  这里我们将客户端程序设置为非阻塞I/O模式,服务器程序不作修改,结果出现运行错误:

  

  

  此处出错误是合理的,因为在非阻塞I/O模式下,模式发生改变,调用connnect方法便出现异常,此外,recv方法在调用时数据可能未准备好,所以这里需要手动实现轮询,如下:

import socket
import threading

HOST, PORT = "localhost", 8020
address = (HOST, PORT)

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(address)
server.listen()
print("server is listening...")
def handlesock(sock, addr):
    while True:
        data = sock.recv(1024).decode("utf-8")
        if data == "exit":
            break
        print("收到来自客户端[{0}]的消息, 内容是[{1}]".format(addr, data))
        response = input("回复[{0}]:	".format(addr)).encode("utf8")
        sock.send(response)

while True:
    sock, addr = server.accept()
    cur_sock = threading.Thread(target=handlesock, args=(sock, addr))
    cur_sock.start()
服务端-阻塞式I/O
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.setblocking(0)

try:
    client.connect(("localhost", 8020))
except BlockingIOError:
    pass

while True:
    response = input("回复服务器:").encode("utf8")
    client.send(response)
    if response=="exit":
        break

    # 非阻塞I/O轮询方式
    while True:
        try:
            data = client.recv(1024)
        except BlockingIOError as e:
            pass
        else:
            if data:
                data = data.decode("utf8")
                break

    print("收到来自服务器的消息:%s" % data)

client.close()
客户端-非阻塞式I/O

  

  

  

原文地址:https://www.cnblogs.com/kisun168/p/11221420.html