Py网络编程及应用(urllib、socket/selectors)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44015805/article/details/100635088
# -*- coding: utf-8 -*-

'''
#Py网络编程及应用.py
(urllib、socket/selectors)


注意:
一、socket模块:
1、通过指定socket类构造器参数type可指定通信协议类型:TCP协议通信、UDP协议通信
2、socket套接字提供两个方法:send 和 recv,分别表示发送和接受数据
3、注意发送与接收数据的 编码解码问题

二、selectors模块:
1、selectors模块使用sel的register方法注册函数,然后使用select方法获取注册事件来准备调用注册函数,
这样以代替常规阻塞式通信的的accept方法循环 send发送数据 及 recv接收数据。


深入:
1、socket通信的 UDP协议通信 多点广播原理和实现  需后期继续了解深入
http://c.biancheng.net/view/2663.html


使用:
一、urllib 模块
urllib 模块可以打开任意 URL 所指向的资源,就像打开本地文件一样,这样程序就能完整地下载远程页面。
如果再与 re 模块结合使用,那么程序完全可以提取页面中各种信息,这就是所谓的“网络爬虫”的初步原理。
1、urllib.request子模块    打开和读取 URL 的各种函数。
结合 多线程threading模块 以及 threading.timer定时器 来创建任务进行 多线程下载 以及 定时循环输出下载完成进度。
2、urllib.parse子模块      用于解析 URL 地址 和 查询字符串的函数:

二、socket模块
socket通信  分为两端:服务器端 和 客户端;
socket通信  通过指定socket类的构造器参数type可指定通信协议类型TCP、UDP等。
socket通信  基本上是一个信息通道,两端各有一个程序;


三、selectors 模块
selectors 模块允许 socket 以非阻塞方式进行通信。
selectors 相当于一个事件注册中心,程序只要将 socket 的所有事件注册给 selectors 管理,当 selectors 检测到 socket 中的特定事件之后,程序就调用相应的监听方法进行处理。
selectors 主要支持两种事件:
1、selectors.EVENT_READ:当 socket 有数据可读时触发该事件。当有客户端连接进来时也会触发该事件。
2、selectors.EVENT_WRITE:当 socket 将要写数据时触发该事件。

'''


# =============================================================================
# #urllib 模块
# #urllib 模块可以打开任意 URL 所指向的资源,就像打开本地文件一样,这样程序就能完整地下载远程页面。
# #如果再与 re 模块结合使用,那么程序完全可以提取页面中各种信息,这就是所谓的“网络爬虫”的初步原理。
# =============================================================================
'''
使用:

一、urllib.request子模块    打开和读取 URL 的各种函数。
1、urllib.request.urlopen(url, data=None) 方法,该方法用于打开 url 指定的资源,并从中读取数据。
根据请求 url 的不同,该方法的返回值会发生动态改变。
如果 url 是一个 HTTP 地址,那么该方法返回一个 http.client.HTTPResponse 对象。

2、urllib.request模块 结合 多线程threading模块 以及 threading.timer定时器 
来创建任务进行 多线程下载 以及 定时循环输出下载完成进度。



二、urllib.parse子模块      用于解析 URL 地址 和 查询字符串的函数:
1、urlparse()    解析URL字符串
2、urlunparse()  将解析后ParseResult对象或元祖 回复称 URL字符串
3、parse_qs() 和 parse_qsl()(这个 l 代表 list)两个函数都用于解析查询字符串,只不过返回值不同而已,
4、urljoin()     函数负责将两个 URL 拼接在一起,返回代表绝对地址的 URL。

'''

import urllib

help(urllib)
urllib.__path__
dir(urllib)

#import os 
#os.startfile(urllib.__path__[0])

#urllib 模块则包含了多个用于处理 URL 的子模块:
help(urllib.request)         #这是最核心的子模块,它包含了打开和读取 URL 的各种函数。
help(urllib.parse)           #用于解析 URL。
#help(urllib.robotparser)     #主要用于解析 robots.txt 文件。(无此子模块)
help(urllib.response)        #
help(urllib.error)           #主要包含由 urllib.request 子模块所引发的各种异常。




####################
#urllib.request 子模块    核心的子模块,它包含了打开和读取 URL 的各种函数。
import urllib

help(urllib.request)
urllib.request.__all__
dir(urllib.request)


help(urllib.request.urlopen)    

help(urllib.request.Request)
help(urllib.request.Request.add_header)


#使用:
#urllib.request.urlopen(url, data=None) 方法,该方法用于打开 url 指定的资源,并从中读取数据。根据请求 url 的不同,该方法的返回值会发生动态改变。如果 url 是一个 HTTP 地址,那么该方法返回一个 http.client.HTTPResponse 对象。
from urllib.request import *

result=urlopen('http://www.crazyit.org/index.php')        #打开URL对应的资源
data=result.read(326)                                     #按字节读取数据
print(data.decode('utf-8'))                               #将字节解码输出

with urlopen('http://www.crazyit.org/index.php') as f:    #上下文管理  打开URL对应的资源
    data=f.read(326)                                      #按字节读取数据
    print(data.decode('utf-8'))                           #将字节解码输出



#使用:
#urlopen() 函数打开远程资源时,第一个 url 参数既可以是 URL 字符串,
#也可以使用 urllib.request.Request 对象。

#urllib.request.Request类对象的的构造器方法来发送POST请求等

#urllib.request.Request 对象的构造器如下:
#urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)



#示例:
#实现了一个多线程下载的工具类:
#通过 urlopen() 函数打开远程资源之后,也可以非常方便地读取远程资源(甚至实现多线程下载)。
from urllib.request import *
import threading


class DownThread(threading.Thread):
    '''
    定义 下载线程 类,继承自线程模块类
    定义 DownThread 线程类,该线程类负责读取从 start_pos 开始、长度为 current_part_size 的所有字节数据,
    并写入本地文件对象中
    '''
    def __init__(self, path, start_pos, current_part_size, current_part):
        super().__init__()
        self.path=path
        self.start_pos=start_pos                         #定义当前线程的下载位置
        self.current_part_size=current_part_size         #定义当前线程负责下载的文件大小
        self.current_part=current_part                   #定义当前线程需要下载的文件快
        self.length=0                                    #定义该线程已经下载的字节数
    def run(self):
        req=Request(url=self.path, method='GET')
        req.add_header('Accept','*/*')
        req.add_header('Charset','UTF-8')
        req.add_header('Connection','Keep-Alive')
        
        f=urlopen(req)
        for i in range(self.start_pos):                 #跳过self.start_pos个字节,表明线程只下载自己负责的那部分内容
            f.read(1)
        
        while self.length < self.current_part_size:     #读取网路数据,并写入本地文件
            data=f.read(1024)
            if data is None or len(data) <= 0:
                break
            self.current_part.write(data)
            self.length += len(data)                    #累计该线程下载的总大小
        self.current_part.close()
        f.close()


class DownUtil:
    '''
    DownUtils 类的 download() 方法负责按如下步骤来实现多线程下载:
    1、使用 urlopen() 方法打开远程资源。
    2、获取指定的 URL 对象所指向资源的大小(通过 Content-Length 响应头获取)。
    3、计算每个线程应该下载网络资源的哪个部分(从哪个字节开始,到哪个字节结束)。
    4、依次创建并启动多个线程来下载网络资源的指定部分。
    '''
    def __init__(self,path, target_file, thread_num):
        self.path=path                                   #定义下载资源的路径
        self.thread_num=thread_num                       #定义线程数量
        self.target_file=target_file                     #定义所下载的文件的保存位置
        self.threads=[]                                  #初始化线程列表
    def download(self):
        req=Request(url=self.path, method='GET')         #实例化创建Request对象
        req.add_header('Accept', '*/*')                  #添加请求头
        req.add_header('Charset','UTF-8')               
        req.add_header('Connection', 'Keep-Alive')       
        
        f=urlopen(req)                                   #打开要下载的资源
        self.file_size=int(dict(f.headers).get('Content-Length',0))  #获取所下载的文件大小:字典的get方法,如果不存在默认返回0
        f.close()                                         
        
        current_part_size=self.file_size // self.thread_num + 1      #计算每个线程要下载的文件大小
        for i  in range(self.thread_num):
            start_pos=i * current_part_size              #计算每个线程的开始位置
            t=open(self.target_file,'wb')                #打开文件进行下载
            t.seek(start_pos,0)                          #定义线程的下载位置
            
            td=DownThread(self.path, start_pos, current_part_size, t)    #创建下载线程
            self.threads.append(td)
            td.start()                                   #启动线程
    def get_complete_rate(self):
        '''获取下载的完成百分比'''
        sum_size=0                                       #统计多个线程已经下载的总大小
        for i in range(self.thread_num):
            sum_size += self.threads[i].length
        return sum_size / self.file_size                 #返回已经完成的百分比
        


# DownUtil 工具类之后,接下来就可以在主程序中调用该工具类的 download() 方法执行下载
du=DownUtil('http://www.crazyit.org/data/attachment/'\
            + 'forum/201801/19/121212ituj1s9gj8g880jr.png','测试文件\\a.png',3)
du.download()                                            #实例化创建对象后开始下载操作

def show_process():
    '''间隔时间连续循环显示完成比例'''
    print('已完成:{:.0f}%'.format((du.get_complete_rate() *100)))
    global t
    if du.get_complete_rate()<1:                         #如果没有100%完成
        t=threading.Timer(0.5,show_process)              #定时器  0.5秒后调用自身函数循环
        t.start()
t=threading.Timer(0.1,show_process)                      #定时器  0.1秒后调用函数
t.start()




#测试  单独输出文件大小
path='http://www.crazyit.org/data/attachment/'\
            + 'forum/201801/19/121212ituj1s9gj8g880jr.png'

req=Request(url=path, method='GET')         #实例化创建Request对象
req.add_header('Accept', '*/*')                  #添加请求头
req.add_header('Charset','UTF-8')               
req.add_header('Connection', 'Keep-Alive')       

f=urlopen(req)                                   #打开要下载的资源
file_size=int(dict(f.headers).get('Content-Length',0))  #获取所下载的文件大小
print(file_size)

f.close() 





####################
#urllib.parse 子模块    用于解析 URL 地址 和 查询字符串的函数:

help(urllib.parse)
urllib.parse.__all__
dir(urllib.parse)

help(urllib.parse.urlparse)    
help(urllib.parse.parse_qs)
help(urllib.parse.parse_qsl)
help(urllib.parse.urlencode)
help(urllib.parse.urljoin)

#1、urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True):
#该函数用于解析 URL 字符串。程序返回一个 ParseResult 对象,可以获取解析出来的数据。

#2、urllib.parse.urlunparse(parts):
#该函数是上一个函数的反向操作,用于将解析结果反向拼接成 URL 地址。

#3、urllib.parse.parse_qs(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace'):
#该该函数用于解析查询字符串(application/x-www-form-urlencoded 类型的数据),并以 dict 形式返回解析结果。

#4、urllib.parse.parse_qsl(qs, keep_blank_values=False, strict_parsing=False, encoding='utf-8', errors='replace'):
#该函数用于解析查询字符串(application/x-www-form-urlencoded 类型的数据),并以列表形式返回解析结果。

#5、urllib.parse.urlencode(query, doseq=False, safe='', encoding=None, errors=None, quote_via=quote_plus):
#将字典形式或列表形式的请求参数恢复成请求字符串。该函数相当于 parse_qs()、parse_qsl() 的逆函数。

#6、urllib.parse.urljoin(base, url, allow_fragments=True):
#该函数用于将一个 base_URL 和另一个资源 URL 连接成代表绝对地址的 URL。




#########
#示例   使用urlparse()函数来解析URL字符串
from urllib.parse import *


#使用:
#urlparse()解析URL字符串
#urlparse()方法解析 URL 字符串,返回一个 ParseResult 对象,该对象实际上是 tuple 的子类。
#因此,程序既可通过属性名来获取 URL 的各部分,也可通过索引来获取 URL 的各部分。
result=urlparse('http://www.crazyit.org:80/index.php;yeeku?name=fkit#frag')
print(result)
print(type(result))

#通过属性名和索引来获取URL的各部分
print('scheme:',      result.scheme, result[0])        #输出 http
print('主机和端口:',   result.netloc, result[1])        #输出 www.crazyit.org:80
print('主机:',        result.hostname)                 #输出 www.crazyit.org
print('端口:',        result.port)                     #输出 80
print('资源路径:',    result.path, result[2])           #输出 index.php
print('参数:',        result.params, result[3])        #输出 yeeku
print('查询字符串:',  result.query, result[4])          #输出 name=fkit
print('fragment:',   result.fragment, result[5])       #输出 frag
print(result.geturl())

#使用:
#urlunparse()将解析后ParseResult对象或元祖 回复称 URL字符串
result=urlunparse(('http','www.crazyit.org:80','index.php','yeeku','name=fkit','frag'))
print('反解析后的URL如下:\n' + result)


#被解析的 URL 以双斜线(//)开头,那么 urlparse() 函数可以识别出主机,只是缺少 scheme 部分。
result=urlparse('//www.crazyit.org:80/index.php')
print('scheme:',        result.scheme, result[0])
print('主机和端口:',     result.netloc, result[1])
print('资源路径:',       result.path, result[2])
print('-----------------')

#被解析的 URL 既没有 scheme,也没有以双斜线(//)开头,那么 urlparse() 函数将会把这些 URL 都当成资源路径。
result=urlparse('www.crazyit.org:80/index.php')
print('scheme:',        result.scheme, result[0])
print('主机和端口:',     result.netloc, result[1])
print('资源路径:',       result.path, result[2])
print('++++++++++++++++++')


#使用:
#parse_qs() 和 parse_qsl()(这个 l 代表 list)两个函数都用于解析查询字符串,只不过返回值不同而已,
#parse_qsl() 函数的返回值是 list(正如该函数名所暗示的)。
#urlencode() 则是它们的逆函数。
result = parse_qs('name=fkit&name=%E7%96%AF%E7%8B%82java&age=12')     #解析查询字符串,返回dict
print(result)

result = parse_qsl('name=fkit&name=%E7%96%AF%E7%8B%82java&age=12')    #解析查询字符串,返回list
print(result)

print(urlencode(result))                                              #将列表格式的请求参数 恢复成 请求参数字符串


#使用:
#urljoin() 函数负责将两个 URL 拼接在一起,返回代表绝对地址的 URL。
#这里主要可能出现 3 种情况:
#1、被拼接的 URL 只是一个相对路径 path(不以斜线开头),那么该 URL 将会被拼接到 base 之后,如果 base 本身包含 path 部分,则用被拼接的 URL替换 base 所包含的 path 部分。
#2、被拼接的 URL 是一个根路径 path(以单斜线开头),    那么该 URL 将会被拼接到 base 的域名之后。
#3、被拼接的 URL 是一个绝对路径 path(以双斜线开头),  那么该 URL将会被拼接到 base 的 scheme 之后。

# 被拼接URL不以斜线开头
result = urljoin('http://www.crazyit.org/users/login.html', 'help.html')
print(result)                                     # http://www.crazyit.org/users/help.html
result = urljoin('http://www.crazyit.org/users/login.html', 'book/list.html')
print(result)                                     # http://www.crazyit.org/users/book/list.html

# 被拼接URL以斜线(代表根路径path)开头
result = urljoin('http://www.crazyit.org/users/login.html', '/help.html')
print(result)                                     # http://www.crazyit.org/help.html

# 被拼接URL以双斜线(代表绝对URL)开头
result = urljoin('http://www.crazyit.org/users/login.html', '//help.html')
print(result)                                     # http://help.html













# =============================================================================
# #socket模块
# #socket模块  良好的封装了基于TCP协议的网络通信,Python使用socket对象来代表两端的通信端口,并通过socket进行网络通信
# #socket通信  分为两端:服务器端 和 客户端
# #socket通信  通过指定socket类的构造器参数type可指定通信协议类型TCP、UDP等
# #socket通信  基本上是一个信息通道,两端各有一个程序
# =============================================================================

'''
注意:
1、通过指定socket类构造器参数type可指定通信协议类型:TCP协议通信、UDP协议通信
2、socket的TCP协议通信机制提供两个方法:send 和 recv,分别表示发送和接受数据
3、注意发送与接收数据的 编码解码问题
4、socket的UDP协议通信机制提供方法:sendto 和 recvfrom,表示发送和接收数据


深入:
1、socket通信的 UDP协议通信 多点广播原理和实现  需后期继续深入
http://c.biancheng.net/view/2663.html


使用:
一、socket的TCP协议通信的 服务器端 编程的基本步骤:
1、服务器端先创建一个 socket 对象。
2、服务器端 socket 将自己绑定到指定 IP 地址和端口。
3、服务器端 socket 调用 listen() 方法监听网络。
4、程序采用循环不断调用 socket 的 accept() 方法接收来自客户端的连接。

二、socket的TCP协议通信的 客户端 也是先创建一个 socket 对象,然
后调用 socket 的 connect() 方法建立与服务器端的连接,这样就可以建立一个基于 TCP 协议的网络连接。
TCP 通信的客户端编程的基本步骤大致归纳如下:
1、客户端先创建一个 socket 对象。
2、客户端 socket 调用 connect() 方法连接远程服务器。

三、多线程实现socket通信    可实现一个命令行界面的 C/S 聊天室应用,

四、socket的UDP协议通信
1、socket.sendto(bytes, address):       发送数据。将 bytes 数据发送到 address 地址。
2、socket.recvfrom(bufsize[, flags]):   接收数据。该方法可以同时返回 socket 中的数据和数据来源地址。


'''
import socket

####################
#探索socket模块
help(socket)
socket.__doc__
socket.__file__
socket.__all__
dir(socket)


help(socket.socket)
#通过socket类的构造器来创建 socket 实例:

#socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

#上面构造器的前三个参数比较重要,其中:
#1、family 参数   用于指定网络类型。
#该参数支持 socket.AF_UNIX(UNIX 网络)、socket.AF_INET(基于 IPv4 协议的网络)和 socket.AF_INET6(基于 IPv6 协议的网络)这三个常量。

#2、type 参数     用于指定网络 Sock 类型。
#type参数可支持 SOCK_STREAM(默认值,创建基于 TCP 协议的 socket)、SOCK_DGRAM(创建基于 UDP 协议的 socket)和 SOCK_RAW(创建原始 socket)。
#一般常用的是 SOCK_STREAM 和 SOCK_DGRAM。
#如果将该参数指定为 SOCK_DGRAM,则意味着创建基于 UDP 协议的 socket。

#3、proto 参数    用于指定协议号,如果没有特殊要求,该参数默认为 0 ,并可以忽略。


####################
#常用类、变量、函数

help(socket.socket)                         #socket通信机制,type参数可支持通信协议类型 SOCK_STREAM(默认值,创建基于 TCP 协议的 socket)、SOCK_DGRAM(创建基于 UDP 协议的 socket)和 SOCK_RAW(创建原始 socket)。
dir(socket.socket)

help(socket.gethostname)                    #获取当前主机名称
help(socket.socket.bind)                    #socket的TCP通信服务器端绑定主机/IP、端口方法
help(socket.socket.listen)                  #socket的TCP通信服务器端监听方法
help(socket.socket.accept)                  #socket的TCP通信服务器端接收连接方法

help(socket.socket.connect)                 #socket的TCP通信客户端连接方法


#socket 对象提供了如下常用方法:

#1、socket.accept():              作为服务器端使用的 socket 调用该方法接收来自客户端的连接。

#2、socket.bind(address):        
#作为服务器端使用的 socket 调用该方法,将该 socket 绑定到指定 address,该 address 可以是一个元组,包含 IP 地址和端口。

#3、socket.close():              关闭连接,回收资源。
#4、socket.connect(address):     作为客户端使用的 socket 调用该方法连接远程服务器。
#5、socket.connect_ex(address):  该方法与上一个方法的功能大致相同,只是当程序出错时,该方法不会抛出异常,而是返回一个错误标识符。

#6、socket.listen([backlog]):    
#作为服务器端使用的 socket 调用该方法进行监听。

#7、socket.makefile(mode='r', buffering=None, *, encoding=None, errors=None, newline=None):
#创建一个和该 socket 关联的文件对象。

#8、socket.recv(bufsize[, flags]):     
#接收socket 中的数据。该方法返回 bytes 对象代表接收到的数据。

#9、socket.recvfrom(bufsize[,flags]):  
#该方法与上一个方法的功能大致相同,只是该方法的返回值是 (bytes, address) 元组。

#10、socket.recvmsg(bufsize[, ancbufsize[, flags]]):
#该方法不仅接收来自 socket 的数据,还接收来自 socket 的辅助数据,因此该方法的返回值是一个长度为 4 的元组 (data, ancdata, msg_flags, address),其中 ancdata 代表辅助数据。

#11、socket.recvmsg_into(buffers[, ancbufsize[, flags]]):
#类似于 socket.recvmsg() 方法,但该方法将接收到的数据放入 buffers 中。

#12、socket.recvfrom_into(buffer[, nbytes[, flags]]):
#类似于 socket.recvfrom() 方法,但该方法将接收到的数据放入 buffer 中。

#13、socket.recv_into(buffer[, nbytes[, flags]]):
#类似于 recv() 方法,但该方法将接收到的数据放入 buffer 中。

#14、socket.send(bytes[, flags]):
#向socket 发送数据,该 socket 必须与远程 socket 建立了连接。该方法通常用于在基于 TCP 协议的网络中发送数据。
 
#15、socket.sendto(bytes, address):
#向 socket 发送数据,该 socket 应该没有与远程 socket 建立连接。该方法通常用于在基于 UDP 协议的网络中发送数据。

#16、socket.sendfile(file, offset=0, count=None):
#将整个文件内容都发送出去,直到遇到文件的 EOF。#17、socket.shutdown(how):关闭连接。其中 how 用于设置关闭方式。




####################
#示例:

#最简单的服务器
#testSocket_hostA.py
'''
使用:
#TCP 通信的服务器端编程的基本步骤:
#1、服务器端先创建一个 socket 对象。
#2、服务器端 socket 将自己绑定到指定 IP 地址和端口。
#3、服务器端 socket 调用 listen() 方法监听网络。
#4、程序采用循环不断调用 socket 的 accept() 方法接收来自客户端的连接。
'''
import socket

s=socket.socket()                          #实例化创建socket对象,默认TCP协议类型通信

host=socket.gethostname()                  #获取计算机全名
port=1234                                  #指定端口号
s.bind((host,port))                        #服务器端首先调用 bind方法绑定主机和端口,注意元组格式
s.listen(2)                                #服务器端其次调用 listen方法来监听特定的地址

#服务器端采用循环不断调用socket的accept()方法接收来自客户端的链接
while True:
    #服务器套接字开始监听后,就可接受客户端连接了,使用方法accept来完成.
    #accept方法将同步阻断等待 到 客户端连接到来为止,然后返回一个(client,address)的元组
    #返回的元组其中client是一个客户端套接字,address是前面解释过的地址。
    c,addr=s.accept()                      #服务器端接收连接。  开始监听后,就可接受客户端连接了,使用方法accept来完成
#    print('Got connection from',addr)
    print(c)
    print('连接地址:',addr)
    
    #注意:传输数据,套接字提供两个方法:send 和 recv,分别表示发送和接受数据
    #注意发送与接收数据的 编码解码问题
    c.send('您好,您收到了来自服务器的新年祝福。'.encode('utf-8'))     #send方法发送数据,提供一个参数为字符串
    c.close()


#注意:
#先运行 服务器端,再运行下面 客户端


#示例
#最简单的客户端
#testSocket_hostB.py
'''
使用:
#TCP 通信的客户端也是先创建一个 socket 对象,然后调用 socket 的 connect() 方法建立与服务器端的连接,这样就可以建立一个基于 TCP 协议的网络连接。
#TCP 通信的客户端编程的基本步骤大致归纳如下:
#1、客户端先创建一个 socket 对象。
#2、客户端 socket 调用 connect() 方法连接远程服务器。
'''
import socket

s=socket.socket()                         #实例化创建socket对象,默认TCP协议类型通信
host=socket.gethostname()                 #获取主机名
port=1234                                 #指定端口号
s.connect((host,port))                    #客户端连接主机和端口 

#注意发送与接收数据的 编码解码问题
print(s.recv(1024).decode('utf-8'))       #recv方法接受数据,提供一个参数为指定最多接受多少个字节的数据





####################
#使用:
#多线程实现socket的TCP通信    实现一个命令行界面的 C/S 聊天室应用,


#服务器端应该包含多个线程,每个 socket 对应一个线程,该线程负责从 socket 中读取数据(从客户端发送过来的数据),
#并将所读取到的数据向每个 socket 发送一次(将一个客户端发送过来的数据“广播”给其他客户端),因此需要在服务器端使用 list 来保存所有的 socket。

#示例
#testSocket_ThreadA.py
#多线程聊天室应用 服务器端

import socket
import threading

socket_list=[]                            #定义保存所有socket的列表

ss=socket.socket()                        #实例化创建socket对象,默认TCP协议类型通信
ss.bind((socket.gethostname(),1235))      #绑定本机主机和端口
ss.listen()                               #服务器端开始监听 来自客户端的链接

def read_from_client(s):
    #定义函数,用来尝试 接收数据,如果没有接收到数据则说明此通信端关闭,则从列表中删除此 通信客户端。
    try:
        return s.recv(2048).decode('utf-8')
    #如果捕获到异常,则表明该socket对应的客户端已经关闭,那么就删除该socket
    except:
        socket_list.remove(s)

def server_target(s):
    #定义了server_target() 函数,用作线程target参数函数
    #该函数将会作为线程执行的 target,负责处理每个 socket 的 通信服务器端。
    try:
        #循环不断的从socket中 读取 来自客户端发送来的数据
        while True:
            content=read_from_client(s)
            print(content)
            if content is None:
                break
            
            #当服务器端线程读取到客户端数据之后,程序遍历 socket_list 列表,
            #并将该数据向 socket_list 列表中的每个 socket 发送一次
            #(该服务器端线程把从 socket 中读取到的数据向 socket_list 列表中的每个 socket 转发一次)
            for client_s in socket_list:
                client_s.send(content.encode('utf-8'))
                
    except e:
        print(e.strerror)

#循环不断准备接收来自客户端的连接,并未每个客户端启动一个线程服务
while True:
    s,addr=ss.accept()                    #准备接收来自客户端的连接  此代码会阻塞
    socket_list.append(s)                 #将对应的socket加入socket_list里表中保存,注意理解是对应的socket,然后开始线程开启。
    #每当客户端连接后,启动一个 线程服务器端 为该 通信客户端客户端 服务
    threading.Thread(target=server_target, args=(s,)).start()



#示例
#testSocket_ThreadB.py
#多线程聊天室应用  客户端  可多开客户端

#每个客户端都应该包含两个线程,
#其中一个  负责读取用户的键盘输入内容,并将用户输入的数据输出到 socket 中,
#另一个    负责读取 socket 中的数据(从服务器端发送过来的数据),并将这些数据打印输出。由程序的主线程负责读取用户的键盘输入内容,由新线程负责读取 socket 数据。

import socket
import threading


s=socket.socket()                         #实例化创建socket对象,默认TCP协议类型通信
s.connect((socket.gethostname(),1235))    #socket通信客户端 使用 connect方法链接 服务器端


def read_from_server(s):
    #定义一个函数,用作线程target参数函数
    #该函数实现不断读取  接收数据  来自服务器发送的数据
    while True:
        print(s.recv(2048).decode('utf-8'))

#客户端启动一个线程  该线程不断地读取来自 服务器端的数据
threading.Thread(target=read_from_server, args=(s,)).start()

#如果程序读取到用户的键盘输入内容,则将内容发送到服务器端
while True:
    line=input('')
    if line is None or line=='exit':
        break
    #将用户的键盘输入内容写入socket服务器端
    s.send(line.encode('utf-8'))








####################
#socket模块的shutdown(how)方法详解
#shutdown(how) 关闭方法,该方法可以只关闭 socket 的输入或输出部分,用以表示输出数据已经发送完成。
import socket

#shutdown 方法的 how 参数接受如下参数值:
#1、SHUT_RD:        关闭 socket 的输入部分,程序还可通过该 socket 输出数据。
#2、SHUT_WR:        关闭该 socket 的输出部分,程序还可通过该 socket 读取数据。
#3、SHUT_RDWR:      全关闭。该 socket 既不能读取数据,也不能写入数据。


#示例
#shutdown() 方法的用法。在该程序中服务器端先向客户端发送多条数据,
#当数据发送完成后,该 socket 对象调用 shutdown() 方法来关闭输出部分,
#表明数据发送结束在关闭输出部分之后,依然可以从 socket 中读取数据。

import socket

s=socket.socket()                                 #实例化创建socket对象,默认TCP协议类型通信
s.bind((socket.gethostname(),1236))               #socket通信服务器端 连接当前主机、端口号
s.listen()                                        #socket通信服务器端 开始监听
skt,addr=s.accept()                               #socket通信服务器端 开始接收连接
skt.send('服务器的第一行数据'.encode('utf-8'))     #socket通信服务器端 发送数据
skt.send('服务器的第二行数据'.encode('utf-8'))     #socket通信服务器端 发送数据

#关闭该 socket通信服务器端 的输出部分,程序还可通过该 socket 读取数据。
skt.shutdown(socket.SHUT_WR)                      #socket通信服务器端 关闭socket的输出,表明输入数据已经结束

while True:
    line=skt.recv(2048).decode('utf-8')
    if line is None or line=='':
        break
    print(line)

skt.close()
s.close()


'''
程序中,关闭了 socket 的输出部分,此时该 socket 并未被彻底关闭,
程序只是不能向该 socket 中写入数据了,但依然可以从该 socket 中读取数据。

当调用 socket 的 shutdown() 方法关闭了输入或输出部分之后,该 socket 无法再次打开输入或输出部分,
因此这种做法通常不适合保持持久通信状态的交互式应用,只适用于一站式的通信协议,
例如 HTTP 协议,即客户端连接到服务器端,开始发送请求数据,当发送完成后无须再次发送数据,
只需要读取服务器端的响应数据即可,当读取响应数据完成后,该 socket 连接就被完全关闭了。
'''







####################
#socket基于UDP协议通信发送和接收数据 及 UDP多点广播


#程序在创建 socket 时,可通过 type 参数指定该 socket 的类型,如果将该参数指定为 SOCK_DGRAM,则意味着创建基于 UDP 协议的 socket。
#在创建了基于UDP 协议的 socket 之后,程序可以通过如下两个方法来发送和接收数据:
#1、socket.sendto(bytes, address):       发送数据。将 bytes 数据发送到 address 地址。
#2、socket.recvfrom(bufsize[, flags]):   接收数据。该方法可以同时返回 socket 中的数据和数据来源地址。

#UDP 协议进行网络通信时,实际上并没有明显的服务器端和客户端,因为双方都需要先建立一个 socket 对象,用来接收或发送数据报。
#但在实际编程中,通常具有固定 IP 地址和端口的 socket 对象所在的程序被称为服务器,因此该 socket 应该调用 bind() 方法被绑定到指定 IP 地址和端口,这样其他 socket(客户端 socket)才可向服务器端 socket(绑定了固定 IP 地址和端口的 socket)发送数据报,而服务器端 socket 就可以接收这些客户端数据报。


##########
#示例:    
#UDP 协议的 socket 实现 C/S 结构的网络通信
#本程序的服务器端通过循环 1000 次来读取 socket 中的数据报,每当读取到内容之后,便向该数据报的发送者发送一条信息。
#服务器端  socket的UDP协议通信 
#testSocketUDP_A.py
import socket

books=('血皇敖天','剑皇霸天',\
       '水心云影','寒天晴岚')                 #定义字符串数组,准备用来 服务器端发送该数据的元素

s=socket.socket(type=socket.SOCK_DGRAM)      #实例化创建socket通信,指定通信协议为UDP协议
s.bind((socket.gethostname(),3000))          #socket通信 服务器端 绑定主机、端口号

for i in range(1000):
    #socket的 UDP协议通信的 recvfrom方法  接收数据  可以同时返回 socket 中的数据 和 数据来源地址。
    data,addr=s.recvfrom(4096)               #接收数据,返回数据 和 来源地址    #4096定义每个数据报的大小最大为4KB
    print(data.decode('utf-8'))
    send_data=books[i % 4].encode('utf-8')   #求余  定义socket通信服务器端 发送数据内容
    #socket的 UDP协议通信的 sendto方法  发送数据  到指定地址
    s.sendto(send_data, addr)                #发送数据,到指定地址
s.close()


#先运行服务器端,再运行客户端


#客户端程序的代码与服务器端类似,
#客户端采用循环不断地读取用户的键盘输入内容,每当读取到用户输入的内容后,就将该内容通过数据报发送出去;接下来再读取来自 socket 中的信息(也就是来自服务器端的数据)。
#客户端    socket的UDP洗衣通信
#testSocketUDP_B.py
import socket

s=socket.socket(type=socket.SOCK_DGRAM)      #实例化创建socket通信,指定通信协议为UDP协议

while True:
    line=input('')
    if line is None or line=='':
        break
    data=line.encode('utf-8')
    #socket的 UDP协议通信的 sendto方法  发送数据  到指定地址
    s.sendto(data,(socket.gethostname(),3000))
    #recv不同于recvfrom
    #socket的 UDP协议通信的 recvfrom方法  接收数据  可以同时返回 socket 中的数据 和 数据来源地址。
    data=s.recv(4096)
    print(data.decode('utf-8'))
s.close()






##########
#UDP多点广播
#多点广播  可以将数据报以广播方式式发送到多个客户端。
#多点广播  则需要将数据报发送到一个组目标地址,当数据报发出后,整个组的所有主机都能接收到该数据报。
#每一个多点广播地址都被看作一个组,当客户端需要发送和接收广播信息时,加入该组即可

#注意:
#DDP多点广播  
#创建了 socket 对象后,还需要将该 socket 加入指定的多点广播地址中,socket 使用 setsockopt() 方法加入指定组。
#如果创建仅用于发送数据报的 socket 对象,则使用默认地址、随机端口即可。
#但如果创建接收数据报的 socket 对象,则需要将该 socket 对象绑定到指定端口;否则,发送方无法确定发送数据报的目标端口。

import socket

#setsockopt()方法  加入指定组
help(socket.socket.setsockopt)

help(socket.IPPROTO_IP)
help(socket.IP_MULTICAST_TTL)

help(socket.SOL_SOCKET)
help(socket.SO_REUSEADDR)

help(socket.IP_ADD_MEMBERSHIP)
help(socket.inet_aton)


#支持多点广播的 socket 还可设置广播信息的 TTL(Time-To-Live),
#该 TTL 参数用于设置数据报最多可以跨过多少个网络:
#1、当TTL的值为 0 时,     指定数据报应停留在本地主机中;
#2、当TTL的值为 1 时,     指定将数据报发送到本地局域网中;
#3、当TTL 的值为 32 时,   意味着只能将数据报发送到本站点的网络上;
#4、当TTL 的值为 64 时,   意味着数据报应被保留在本地区;
#5、当TTL 的值为 128 时,  意味着数据报应被保留在本大洲;
#6、当TTL 的值为 255 时,  意味着数据报可被发送到所有地方;


#注意:UDP多点广播  需后期继续深入












# =============================================================================
# #selectors 模块
# #selectors 模块允许 socket 以非阻塞方式进行通信。

# #selectors 相当于一个事件注册中心,程序只要将 socket 的所有事件注册给 selectors 管理,当 selectors 检测到 socket 中的特定事件之后,程序就调用相应的监听方法进行处理。
# #selectors 主要支持两种事件:
# #1、selectors.EVENT_READ:当 socket 有数据可读时触发该事件。当有客户端连接进来时也会触发该事件。
# #2、selectors.EVENT_WRITE:当 socket 将要写数据时触发该事件。
# =============================================================================

'''
注意:
1、selectors模块使用sel的register方法注册函数,然后使用select方法获取注册事件来准备调用注册函数,
这样以代替常规阻塞式通信的的accept方法循环 send发送数据 及 recv接收数据。


使用:
一、selectors模块 实现非阻塞式编程的步骤大致如下:
1、创建 selectors 对象。
2、通过 selectors 对象为 socket 的 selectors.EVENT_READ 或 selectors.EVENT_WRITE 事件注册监听器函数。每当 socket 有数据需要读写时,系统负责触发所注册的监昕器函数。
3、在监听器函数中处理 socket 通信。

'''

import selectors

help(selectors)
selectors.__doc__
selectors.__file__
dir(selectors)


help(selectors.EVENT_READ)
dir(selectors.EVENT_READ)

help(selectors.EVENT_WRITE)
dir(selectors.EVENT_WRITE)


####################
#常用类及属性

help(selectors.DefaultSelector())             #默认的selectors对象,实例化创建后 可以调用 类方法 进行事件注册
dir(selectors.DefaultSelector)                #默认的selectors对象,实例化创建后 可以调用 类方法 进行事件注册

help(selectors.DefaultSelector.register)      #注册事件
help(selectors.DefaultSelector.unregister)    #取消注册事件
help(selectors.DefaultSelector.select)        #获取注册事件,返回列表(key,events)



###################
#示例    

##########
#使用selectors模块实现非阻塞式socket通信服务器端程序
#服务器端:
import selectors, socket

#实例化创建selectors的默认对象,然后可以调用 类方法sel.register() 进行事件注册。
sel=selectors.DefaultSelector()                #创建默认的selectors对象
socket_list=[]


def read(skt, mask):
    '''负责监听"有数据可读"事件的函数
    尝试:如果读取到数据则将读取到的数据发送给每个socket通信客户端,否则就是通信退出  则关闭该socket通信客户端,并从列表中删除。
    异常:则 取消 socket通信客户端 的 注册事件,并关闭该socket通信客户端,并将其从列表中删除。
    
    参数skt  为一个socket通信客户端连接。
    '''
    #尝试:读取数据,并将读取到的数据发送给每个socket_list
    try:
        data=skt.recv(1024)
        #如果读取到数据,则将数据循环发送给每个socket_list
        if data:
            for s in socket_list:
                s.send(data)
        #如果没有读取到数据,则 关闭 socket的通信客户端连接 并 从socket_list中删除此 该通信客户端连接
        else:
            print('关闭',skt)
            skt.close()                        #socket的通信客户端关闭
            socket_list.remove(skt)            #从列表中删除socket的通信客户端
    #如果异常:将该socket关闭,并从socket_list列表中删除
    except:
        print('关闭',skt)
        #取消注册事件
        sel.unregister(skt)                    #取消skt的注册事件
        skt.close()                            #socket的通信客户端关闭
        socket_list.remove(skt)                #从列表中删除socket的通信客户端


def accept(sock, mask):
    '''负责监听“有客户端连接进来”事件的函数'''
    conn,addr=sock.accept()                    #socket通信服务器端 准备接收 客户端连接
    socket_list.append(conn)                   #socket_list添加保存接收到的 socket通信客户端
    conn.setblocking(False)                    #设置socket为非阻塞式
    
    #sel.register类方法  注册事件  
    #即:为conn的READ事件注册监听函数read,用来读取后循环输出给每一个socket通信客户端。
    sel.register(conn, selectors.EVENT_READ, read)    #函数内注册READ事件监听函数read


#创建socket通信服务器端的顺序步骤
sock=socket.socket()                           #实例化创建通信
sock.bind((socket.gethostname(), 1236))        #socket通信服务器端  绑定本机主机名、端口号
sock.listen()                                  #执行监听
sock.setblocking(False)                        #设置该socket是非阻塞式的

#注意:使用sel的register注册方法注册函数,然后使用select方法获取注册事件来调用注册函数,
#这样以代替常规阻塞式通信的的accept方法循环 send发送数据 及 recv接收数据。

#使用sel为 sock的EVENT_READ事件 注册read监听函数
sel.register(sock, selectors.EVENT_READ, accept)    #为对象sock注册事件函数  即为socket服务器端注册监听函数事件
#register(self, fileobj, events, data=None)

#采用死循环方式  不断提取sel的事件
while True:
    #sel.select()类方法  获取注册事件  返回列表结构  元素包含两个内容(key,events)
    events=sel.select()
    
    for key,mask in events:
        #key的data属性  获取为该事件注册的监听函数
        callback=key.data
        #key的fileobj属性  获取被监听的socket对象
        callback(key.fileobj, mask)            #执行key.data获取到的监听函数





##########
#使用selectors模块实现非阻塞式socket通信客户端程序
#客户端:
import selectors, socket, threading

sel=selectors.DefaultSelector()                #创建默认的selectors对象

def read(conn, mask):
    data=conn.recv(1024)
    if data:
        print(data.decode('utf-8'))
    else:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()

s=socket.socket()                              #创建socket对象
s.connect((socket.gethostname(),1236))         #socket通信客户端  连接 本机主机名、端口
s.setblocking(False)                           #设置该socket为非阻塞式


#使用sel为 s 的EVENT_READ事件 注册 read监听函数
sel.register(s, selectors.EVENT_READ, read)    


def keyboard_input(s):
    while True:
        line=input('')
        if line is None or line=='exit':
            break
        s.send(line.encode('utf-8'))
#创建线程执行函数
threading.Thread(target=keyboard_input, args=(s,)).start()

while True:
    #sel.select()类方法  获取注册事件  返回列表结构  元素包含两个内容(key,events)
    events=sel.select()
    for key,mask in events:
        #key的data属性 获取为该事件注册的监听函数 
        callback=key.data
        #key的fileobj属性  获取被监听的socket对象
        callback(key.fileobj, mask)            #执行key.data获取到的监听函数












猜你喜欢

转载自blog.csdn.net/weixin_44015805/article/details/100635088