python中socket编程

网络通信三要素

  1. IP地址
    (1) 用来标识网络上一台独立的主机
    (2) IP地址 = 网络地址 + 主机地址(网络号:用于识别主机所在的网络/网段。主机号:用于识别该网络中的主机)
    (3) 特殊的IP地址:127.0.0.1(本地回环地址、保留地址,点分十进制)可用于简单的测试网卡是否故障。表示本机。
  2. 端口号
    (1) 用于标识进程的逻辑地址。不同的进程都有不同的端口标识。
    (2) 端口:要将数据发送到对方指定的应用程序上,为了标识这些应用程序,所以给这些网络应用程序都用数字进行标识。为了方便称呼这些数字,则将这些数字称为端口。(此端口是一个逻辑端口)
  3. 传输协议
    通讯的规则。例如:TCP、UDP协议(好比两个人得用同一种语言进行交流)
    ①、UDP:User Datagram Protocol用户数据报协议
    特点:
    面向无连接:传输数据之前,源端和目的端不需要建立连接。
    每个数据报的大小都限制在64K(8个字节)以内。
    面向报文的不可靠协议。(即:发送出去的数据不一定会接收得到)
    传输速率快,效率高。
    现实生活实例:邮局寄件、实时在线聊天、视频会议等。
    ②、TCP:Transmission Control Protocol传输控制协议
    特点:
    面向连接:传输数据之前需要建立连接。
    在连接过程中进行大量数据传输。
    通过“三次握手”的方式完成连接,是安全可靠协议。
    传输速度慢,效率低

网络通讯步骤:
确定对端IP地址→ 确定应用程序端口 → 确定通讯协议
总结:网络通讯的过程其实就是一个(源端)不断封装数据包和(目的端)不断拆数据包的过程。
在这里插入图片描述

简单来说就是:发送方利用应用软件将上层应用程序产生的数据前后加上相应的层标识不断的往下层传输(封包过程),最终到达物理层。通过看得见摸得着的物理层设备,例如:网线、光纤…等将数据包传输到数据接收方,然后接收方则通过完全相反的操作不断的读取和去除每一层的标识信息(拆包过程),最终将数据传递到最高层的指定的应用程序端口,并进行处理。

SOCKET 编程:
要想理解socket,就要先来理解TCP,UDP协议
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,定义了主机如何连入因特网及数据如何在它们之间传输的标准,
TCP/IP协议是指因特网整个TCP/IP协议族。
TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中:

  • 应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等
  • 传输层:TCP,UDP
  • 网络层:IP,ICMP,OSPF,EIGRP,IGMP
  • 数据链路层:SLIP,CSLIP,PPP,MTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的。

我们可以利用ip地址+协议+端口号唯一标示网络中的一个进程。能够唯一标示网络中的进程后,它们就可以利用socket进行通信了,我们经常把socket翻译为套接字,socket是在应用层和传输层(TCP/IP协议族通信)之间的一个抽象层,是一组接口,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
应用程序两端通过“套接字”向网络发出请求或者应答网络请求。可以把socket理解为通信的把手(hand)
Socket是实现TCP,UDP协议的接口,便于使用TCP,UDP。

在python中,socket源码里面:

class socket(_socket.socket):
    """A subclass of _socket.socket adding the makefile() method."""
    __slots__ = ["__weakref__", "_io_refs", "_closed"]

    def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):
        # family 和 type 有默认值,默认为服务器之间建立TCP通信。
        # AF_INET :服务器之间的通信(ipv4)
		# AF_INET6 :服务器之间的通信(ipv6)
        # AF_UNIX :Unix 不同进程之间的通信
        # SOCK_STREAM : 建立TCP时的参数
        # SOCK_Dgram  : 建立UDP时的参数
        _socket.socket.__init__(self, family, type, proto, fileno)
        self._io_refs = 0
        self._closed = False

socket通信流程如图:
在这里插入图片描述

根据上图顺序写出代码:
说明:建立2个py文件,原则上应该是在2台电脑上各自建立1个py文件,分别是 server.py 和 client.py 。由于现在只有一台电脑,所以建立2个py文件。

server.py:

import socket

sk = socket.socket()
sk.bind(('127.0.0.1',8000))   # 通过sk中的bind()方法,绑定地址和端口,参数必须是元组
sk.listen(3)   # 设置访问程序的最大排队人数
conn, address = sk.accept()
inp = input('请输入需要传输的内容:')
conn.send(bytes(inp,'utf8'))   # 传输的内容一定是byte类型

client.py

import socket

sk = socket.socket()
sk.connect(('127.0.0.1',8000))
data = sk.recv(1024)
print(str(data,'utf8'))  # str()转换

改进为可以一直聊天的:
server.py:

import socket

sk = socket.socket()
address = ('127.0.0.1',8000)   # 实际应用中写需要连接的IP地址和端口
sk.bind(address)   # 通过sk中的bind()方法,绑定地址和端口,参数必须是元组
sk.listen(3)   # 设置访问程序的最大排队人数
conn, address = sk.accept()
while True:
    inp = input('你说:')
    conn.send(bytes(inp,'utf8'))   # conn.send() 传输的内容一定是byte类型
    data = conn.recv(1024)       #conn.recv()
    print('对方说:',str(data,'utf8'))
conn.close()

client.py

import socket

sk = socket.socket()
address = ('127.0.0.1',8000)
sk.connect(address)
while True:
    data = sk.recv(1024)     # sk.recv()
    print('对方说:',str(data,'utf8'))
    inp = input('你说:')
    sk.send(bytes(inp,'utf8'))   # sk.send() 传输的内容一定是byte类型
sk.close()

socket中的方法:(不一定都是sk.**())

sk.bind(address)
  #sk.bind(address) 将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址。

sk.listen(backlog)
   #开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。
    #backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5。
    #这个值不能无限大,因为要在内核中维护连接队列

sk.setblocking(bool)
  #是否阻塞(默认True),如果设置False,那么accept和recv时一旦无数据,则报错。

sk.accept()
  #接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址。
  #接收TCP 客户的连接(阻塞式)等待连接的到来

sk.connect(address)
  #连接到address处的套接字。一般,address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

sk.connect_ex(address)
  #同上,只不过会有返回值,连接成功时返回 0 ,连接失败时候返回编码,例如:10061

sk.close()
  #关闭套接字

sk.recv(bufsize[,flag])
  #接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

sk.recvfrom(bufsize[.flag])
  #与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

sk.send(string[,flag])
  #将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。
   #传输的内容一定是byte类型

sk.sendall(string[,flag])
  #将string中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。
   #内部通过递归调用send,将所有内容发送出去。

sk.sendto(string[,flag],address)
  #将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。

sk.settimeout(timeout)
  #设置套接字操作的超时期,timeout是一个浮点数,单位是秒。值为None表示没有超时期。一般,超时期应该在刚创建套接字时设置,因为它们可能用于连接的操作(如 client 连接最多等待5s )

sk.getpeername()
  #返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

sk.getsockname()
  #返回套接字自己的地址。通常是一个元组(ipaddr,port)

sk.fileno()
  #套接字的文件描述符

远程执行命令
tips1:
bytes()函数返回一个新的 bytes 对象,该对象是一个 0 <= x <=255 区间内的整数不可变序列(十六进制)。因为在计算机存储和传输是都要以二进制的形式。

# bytes 语法:

class bytes([source[, encoding[, errors]]])

# 如果source为整数,则返回一个长度为source的初始化数组;
# 如果 source 为字符串,则按照指定的 encoding 将字符串转换为字节序列;
# 如果 source 为可迭代类型,则元素必须为[0 ,255] 中的整数;
# 如果 source 为与 buffer 接口一致的对象,则此对象也可以被用于初始化 bytearray。
# 如果没有输入任何参数,默认就是初始化数组为0个元素。

注意:source不能是int类型,如果需要转换int类型,可以先把int类型转化为字符串str()类型

tips2:
用subprocess的Popen创建并返回一个子进程,并在这个进程中执行指定的程序。。
Popen的构造函数例如以下:

subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None,
                 close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None,
                 creationflags=0)

具体参数的含义使用的时候再百度。

远程执行命令的代码:
server.py(服务端)

import socket
import subprocess

ip_port = ('127.0.0.1',8879)
sk = socket.socket()
sk.bind(ip_port)
sk.listen(5)
print ("服务端启动...")
while True:
    conn, address = sk.accept()
    while True:
        try:
            client_data = conn.recv(1024)
        except Exception:
            break
        print(str(client_data,"utf8"))
        print("waiting...")
        cmd = str(client_data,"utf8").strip()
        cmd_call = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE)
        cmd_result = cmd_call.stdout.read()  # 读取子进程的实时输出结果
        if len(cmd_result)==0:
            cmd_result=b"no output!"
        conn.sendall(cmd_result)
        print('send data size',len(cmd_result))
        print('******************')
    conn.close()

client.py(客户端)

import socket
ip_port = ('127.0.0.1',8879)
sk = socket.socket()
sk.connect(ip_port)
print("客户端启动:")
while True:
    inp = input('cdm:>>>').strip()
    if len(inp)==0:
        continue
    if inp=="q":
        break
    sk.sendall(bytes(inp,"utf8"))
    server_response=sk.recv(1024)
    print (str(server_response,"gbk"))
    print('receive data size',len(server_response))
    if inp == 'exit':
        break
sk.close()

注意:程序中如果连续使用2个.send(),就可能会造成粘包现象。
解决办法:在两个.send()中间加上.recv()。(对应地,在另一端加.send()

编码拾遗
py3中,只有2种编码类型。str 和 bytes
str : unicode
bytes : 十六进制

str ----> bytes : 编码
bytes ----> str : 解码

s = 'hello你好'

# 编码方式一:
b1 = bytes(s, 'utf8')
print(b1)   # >>> b'hello\xe4\xbd\xa0\xe5\xa5\xbd'
            # >>> 这是utf8规则下的bytes类型
            # utf8 中 ,hello在ASCII表中,不变
            # 中文‘你’占三个字节,xe4\xbd\xa0
            # 中文‘好’占三个字节,xe5\xa5\xbd

# 编码方式二:
b2 = s.encode('utf8')
print(b2)   # >>> b'hello\xe4\xbd\xa0\xe5\xa5\xbd'

# 解码方式一:
s1 = str(b2,'utf8')
print(s1)   # >>> hello你好

# 解码方式二:
s2 = b2.decode('utf8')
print(s2)   # >>> hello你好

注意:编码和解码必须使用同一规则,要么都是utf8,要么都是gbk

socketserver框架:
一.使用socketserver的原因:
在python的socket编程中,使用socket模块的时候,是不能实现多个连接的,当然如果加入其它的模块是可以的,例如socketserver模块。
socket不支持多并发,socketserver最主要的作用:就是实现一个并发处理。

二. socketserver模块简介
socketserver框架是一个基本的socket服务器端框架, 使用了threading来处理多个客户端的连接, 使用seletor模块来处理高并发访问, 是值得一看的python 标准库的源码之一

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # 。。。
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer((127.0.0.1,8098),MyServer)
    # 执行顺序:
    #   1.创建了ThreadingTCPServer()类的实例,并传入两个参数
    #   2.执行__init__方法(执行且只执行一个)
    #   3.先找ThreadingTCPServer()类的__init__方法
    #   4.步骤3没找到,找ThreadingTCPServer()类的父类的__init__方法
    #     class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
    #   5.找左边父类ThreadingMixIn()类的__init__方法
    #   6.步骤5没找到,找右边父类TCPServer()的__init__方法,并执行。
    server.serve_forever()

三. socketserver共提供5个服务类型:
TCP 协议、UDP 协议、Unix 本机之间进程的TCP、UDP (不常用)
类型关系:
在这里插入图片描述

四. 创建一个socketserver 至少分以下几步:

  • 首先,必须创建一个请求处理类,继承BaseRequestHandlerclass类并且重写父类的handle()方法,该方法将处理传入的请求。
  • 其次,必须实例化一个上面类型中的一个类(如TCPServer)传递服务器的地址和你上面创建的请求处理类 给这个TCPServer。
  • 然后,调用handle_request()或者serve_forever()方法来处理一个或多个请求
  • 最后,调用server_close()关闭socket。

五. socketserver服务器端和客户端代码
服务器

import socketserver

ip_port = ("127.0.0.1", 8000)
class MyServer(socketserver.BaseRequestHandler):
    def Handle(self):
        print("conn is :", self.request)  # conn
        print("addr is :", self.client_address)  # addr
        while True:
            try:
                # 收消息
                data = self.request.recv(1024)
                if not data: break
                print("收到客户端的消息是", data.decode("utf8"))
                # 发消息
                self.request.sendall(data.upper())
            except Exception as e:
                print(e)
                break

if __name__ == "__main__":
    s = socketserver.ThreadingTCPServer(ip_port, MyServer)
    s.serve_forever()

客户端的代码和socket编程的代码基本相同。

代码工作流程:
程序 > 解释器 > 操作系统 > cpu/disk/ram
在这里插入图片描述

线程和进程:

  • 线程:
    操作系统能够进行运算调度的最小单位。 它包含在进程之中,是进程的实际运作单位。 一条线程指的是进程中一个单一顺序的控制流, 一个进程中可以并发多个线程,每一条线程并行执行不同的任务。 通俗的说,一个线程就是一堆指令集。 创建线程:

    
    def foo(n):
        print('%s'%n)
    
    def bar(n):
        print('%s'%n)
    
    print('33')   # 主线程
    
    t1 = threading.Thread(target=foo,args=(1,))    # 线程一 
    t2 = threading.Thread(target=bar,args=(2,))    # 线程二 
    t1.start() 
    t2.start()
    # 创建线程,第一个参数是函数名字(不加括号,加括号就执行函数了)。
    # 第二个参数是要传给函数的参数,以元组的形式。
    # 创建线程之后, .start()启动线程。 ```
    
  • 进程:
    对一堆资源的整合。 比如说QQ就是一个进程。
    目的:最大限度的利用CPU,节省时间。

多线程并不会充分调用两个CPU,而是会在一个CPU上充分运转;
而多进程则是会完全调用两个CPU,同时执行;

陷阱题:进程和线程谁执行的快?
答:一样快
理由:从执行的角度来说,进程和线程执行的是同样的代码。

并行:多个CPU同时执行多个任务,就好像有两个程序,这两个程序是真的在两个不同的CPU内同时被执行。
并发:CPU交替处理多个任务,还是有两个程序,但是只有一个CPU,会交替处理这两个程序,而不是同时执行,只不过因为CPU执行的速度过快,而会使得人们感到是在“同时”执行,执行的先后取决于各个程序对于时间片资源的争夺。

python的GIL(Global Interpreter Lock)全局解释器锁。
Guido van Rossum(吉多·范罗苏姆)创建python时就只考虑到单核cpu,解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁,于是有了GIL这把超级大锁。因为cpython解析只允许拥有GIL全局解析器锁才能运行程序,这样就保证了在同一个时刻只允许一个线程可以使用cpu。

什么是GIL?
每个线程在执行时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU,也就是说多线程并不是真正意义上的同时执行。

解决方案:用多进程。

多线程的弊端
同一个任务(多条语句)在进行多线程处理时候,可能会发生线程切换太快的现象而导致任务结果出错。

import threading
def addNum():
    global num  # 在每个线程中都获取这个全局变量
    # num -= 1
    tem = num
    print('此时可能会发生线程切换')
    num = tem - 1

num = 100
thread_list = []
for i in range(100):
    t = threading.Thread(target=addNum)
    t.start()
    thread_list.append(t)    # 线程添加到列表里面

for t in thread_list:
    t.join()

print('final num :', num)

这段代码的预计结果是0,但实际结果可能不是0。
在这里插入图片描述

解决办法:
加锁

r = threading.Lock()
def addNum():
    global num  # 在每个线程中都获取这个全局变量
    r.acquire()
    tem = num
    print('此时可能会发生线程切换')
    num = tem - 1
    r.release()

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。
解决办法:使用递归锁

信号量
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。(类似于停车位的概念)
BoundedSemaphore() 与Semaphore() 的唯一区别在于前者将在调用release() 时检查计数器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。

import threading,time

class myThread(threading.Thread):
    def run(self):
        if semaphore.acquire():
            print(self.name)
            time.sleep(2)
            semaphore.release()
if __name__=="__main__":
    semaphore=threading.Semaphore(5)   # 信号量设置为5
    thrs=[]
    for i in range(100):
        thrs.append(myThread())
    for t in thrs:
        t.start()

每次打印出5个线程(无序)

条件变量
有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。

  • wait():条件不满足时调用,线程会释放锁并进入等待阻塞;
  • notify():条件创造后调用,通知等待池激活一个线程;
  • notifyAll():条件创造后调用,通知等待池激活所有线程。

多线程利器:队列
队列:默认先进先出(FIFO)
队列里面有一把锁,保证数据的安全。
创建一个“队列”对象

import Queue
q = Queue.Queue(maxsize = 10)

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。默认为0

将一个值放入队列中

q.put(10)

调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

将一个值从队列中取出

q.get()

调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

Python Queue模块有三种队列及构造函数:

  • Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize)
  • LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize)
  • 还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)

此包中的常用方法(q = Queue.Queue()):

  • q.qsize() 返回队列的大小

  • q.empty() 如果队列为空,返回True,反之False

  • q.full() 如果队列满了,返回True,反之False

  • q.full 与 maxsize 大小对应

  • q.get([block[, timeout]]) 获取队列,timeout等待时间

  • q.get_nowait() 相当q.get(False)非阻塞

  • q.put(item) 写入队列,timeout等待时间

  • q.put_nowait(item) 相当q.put(item, False)

  • q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号

  • q.join() 实际上意味着等到队列为空,再执行别的操作

多进程
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing,只需要定义一个函数,Python会完成其他所有事情。借助这个包,可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

from multiprocessing import Process
import time

def f(name):
    time.sleep(1)
    print('hello', name, time.ctime())

if __name__ == '__main__':
    p_list = []
    for i in range(3):
        p = Process(target=f, args=('alvin',))
        p_list.append(p)
        p.start()
    for i in p_list:
        i.join()
    print('end')

同时打印:
hello alvin Mon May 27 18:46:39 2019
hello alvin Mon May 27 18:46:39 2019
hello alvin Mon May 27 18:46:39 2019
end

协程:
一种用户态的轻量级线程
说白了就是程序在一个单线程上跑。

事件驱动:为了提高效率,节省CPU
进程的阻塞:进程的阻塞是进程自身的一种主动行为(正在执行的进程,由于期待的某些事未发生,比如请求系统资源失败、等待某种操作的完成、新数据尚未到达,则由系统自动执行阻塞原语block,使自己有运行状态进入阻塞状态),当进程进入阻塞状态,是不占用CPU的。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值