网络编程

Socket编程

一、客户端/服务端架构

Socker就是为了完成C/S架构的开发,客服端/服务端架构即C/S架构。包括:硬件C/S架构(比如:打印机),软件C/S架构(比如网站是服务端,你的浏览器是客户端)。

二、OSI七层

在学socket之前首先要学习互联网协议,对ios七层有一定的了解。

三、socket层

在上面图中,我们并没看到socket层,那么它到底在哪一层呢?看下图就能看出

从图中可以看出socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,socket其实就是一个门面模式,它吧复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。

四、套接字

1.套接字的分类

套接字有两种,分别是基于文件型的(AF_UNIX)和基于网络型的(AF_INET)。

2.套接字工作流程

套接字的工作流程很像生活中打电话的场景。你给一个朋友打电话,首先拨号,朋友听到电话铃声后接听电话,这时候你们就建立起了连接,就可以进行通信了。等通信结束,挂断电话结束此次通信。下面我来就用图来分析套接字的工作流程。

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

下面我们来介绍socket模块的用法:

import socket   #导入socket模块

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #获取tcp/ip套接字
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  #获取udp/ip套接字

套接字函数:

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

#客户端套接字函数
#.connect()     主动初始化TCP服务器连接
#.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常
#公共用途的套接字函数 #.recv() 接收TCP数据 #.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完) #.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完) #.recvfrom() 接收UDP数据 #.sendto() 发送UDP数据 #.getpeername() 连接到当前套接字的远端的地址 #.getsockname() 当前套接字的地址 #.getsockopt() 返回指定套接字的参数 #.setsockopt() 设置指定套接字的参数 #.close() 关闭套接字 #面向锁的套接字函数 #.setblocking() 设置套接字的阻塞与非阻塞模式 #.settimeout() 设置阻塞套接字操作的超时时间 #.gettimeout() 得到阻塞套接字操作的超时时间 #面向文件的套接字函数 #.fileno() 套接字的文件描述符 #.makefile() 创建一个与该套接字相关的文件

3.基于TCP的套接字

tcp是基于链接的,必须先启动服务端,然后再客户端去链接服务端。

下面我们来看一个简单的基于tcp的套接字通信,模拟打电话流程:

#服务端
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机,socket.AF_INET基于网络通信,socket.SOCK_STREAM代表TCP协议
phone.bind(('127.0.0.1',8000))  #绑定手机卡
phone.listen(5)   #开机,listen相当于半连接池
print('等电话中')
conn,addr = phone.accept() #等电话  #conn电话链接、addr对方的手机号

msg = conn.recv(1024)  #收消息
print('客户端发来的消息是:',msg)
conn.send(msg.upper())  #发消息

conn.close()   #挂电话
phone.close()   #关机

#客户端
import socket

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)  #买手机

phone.connect(('127.0.0.1',8000))  #拨通电话

phone.send('hello'.encode('utf-8'))  #发消息
data = phone.recv(1024)   #收消息
print('收到服务端发来的消息是:',data)

上面代码只能实现一次链接和一次通信,那么我们怎么实现多次链接和多次通信呢?请看下面代码实现:

from socket import *
ip_port = ('127.0.0.1',8080)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)
while True:
    conn,addr = tcp_server.accept()   #服务端阻塞
    print('双向链接是',conn)
    print('客户端地址',addr)

    while True:
        try:
            data = conn.recv(buffer_size)
            print('客户端发来的消息是',data.decode('utf-8'))
            conn.send(data.upper())
        except Exception:
            break

    conn.close()
tcp_server.close()
服务端
from socket import *
ip_port = ('127.0.0.1',8080)
back_log = 5
buffer_size = 1024

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)
while True:
    msg = input('>>:').strip()
    if not msg:continue
    tcp_client.send(msg.encode('utf-8'))
    print('客户端已经发送消息')
    data = tcp_client.recv(buffer_size)
    print('收到服务端发来的消息',data.decode('utf-8'))

tcp_client.close()
客户端

在这里,客户端可以开多个,只不过不能实现并发。

其中有一个问题可能会在重启服务端时遇到:

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址。

解决方法:加入一条socket配置,重用ip和端口,即

phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))

4.基于UDP的套接字

 udp是无链接的,先启动哪一端都不会报错。

下面我们来实现简单udp通信:

from socket import *
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM代表的udp协议
udp_server.bind(ip_port)

while True:
    data,addr = udp_server.recvfrom(buffer_size)
    print(data)
    udp_server.sendto(data.upper(),addr)
服务端
from socket import *
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM代表的udp协议

while True:
    msg = input('>>:').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)
    data,addr = udp_client.recvfrom(buffer_size)
    print(data.decode('utf-8'))
客户端

由于udp无连接,所以可以同时多个客户端去跟服务端通信。

时间服务器也是基于udp的套接字实现的,具体代码如下:

from socket import *
import time
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM代表的udp协议
udp_server.bind(ip_port)

while True:
    data,addr = udp_server.recvfrom(buffer_size)
    print(data)
    if not data:
        fmt = '%Y-%m-%d %X'
    else:
        fmt = data.decode('utf-8')
    back_time = time.strftime(fmt)
    udp_server.sendto(back_time.encode('utf-8'),addr)
服务端
from socket import *
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM) #SOCK_DGRAM代表的udp协议

while True:
    msg = input('>>:').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)
    data,addr = udp_client.recvfrom(buffer_size)
    print('ntp服务器的标准时间是',data.decode('utf-8'))
客户端

五、粘包

1.粘包现象

我们先看一个简单的粘包现象,代码如下:

from socket import *
ip_port = ('127.0.0.1',8080)
back_log = 5
buffer_size = 1024

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

conn,addr = tcp_server.accept()

data = conn.recv(buffer_size)
print('第一次数据',data)
data1 = conn.recv(buffer_size)
print('第二次数据',data1)
data2 = conn.recv(buffer_size)
print('第三次数据',data2)
服务端
from socket import *
ip_port = ('127.0.0.1',8080)
back_log = 5
buffer_size = 1024

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('world'.encode('utf-8'))
tcp_client.send('alex'.encode('utf-8'))
客户端

这是一个基于tcp的套接字,我们先执行服务端,再执行客户端,看看结果是啥?

第一次数据 b'helloworldalex'
第二次数据 b''
第三次数据 b''

很明显客户端发的数据都一次被服务端接收,这就是属于一种粘包现象。

既然基于tcp的套接字会发生粘包现象,那么基于udp的套接字是否会发生?我们在基于udp写一段代码:

from socket import *
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_server = socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)

data = udp_server.recvfrom(buffer_size)
print('第一次',data)
data1 = udp_server.recvfrom(buffer_size)
print('第二次',data1)
data2 = udp_server.recvfrom(buffer_size)
print('第三次',data2)
服务端
from socket import *
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

udp_client = socket(AF_INET,SOCK_DGRAM)

udp_client.sendto(b'hello',ip_port)
udp_client.sendto(b'world',ip_port)
udp_client.sendto(b'alex',ip_port)
客户端

执行程序的结果为:

第一次 (b'hello', ('127.0.0.1', 52753))
第二次 (b'world', ('127.0.0.1', 52753))
第三次 (b'alex', ('127.0.0.1', 52753))

很明显基于udp的套接字,在运行时不会发生粘包。

2.什么是粘包

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

我要明白只有tcp有粘包现象,udp永远不会粘包,为什么呢?我们首先需要掌握一个socket收发消息的原理,根据下图我们进行讲解:

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

那么在什么情况下会发生粘包?

  • 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
  • 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

我们上面写的粘包属于第一种。第二种的具体实现如下:

from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()
服务端
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello feng'.encode('utf-8'))
客户端

3.解决粘包方法

粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

第一种解决方法:

import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客户端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
                            stdin=subprocess.PIPE,\
                         stderr=subprocess.PIPE,\
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()
        data_length=len(ret)
        conn.send(str(data_length).encode('utf-8'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()
服务端
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))
客户端

第一种解决方法程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗。

第二种解决方法:

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

phone.bind(('127.0.0.1',8080))

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()


        conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
        conn.sendall(back_msg) #在发真实的内容

    conn.close()
服务端
import socket,time,struct

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))



    l=s.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)

    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默认gbk编码
客户端

第二种解决方法为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。在这个我们用到了struct模块,该模块可以把一个类型转成固定长度的bytes。

六、认证客户端的链接合法性

如何在分布式系统中实现一个简单的客户端链接认证功能呢?我们可以利用hmac+加盐的方式来实现:

from socket import *
import hmac,os

secret_key=b'hello'
def conn_auth(conn):
    '''
    认证客户端链接
    :param conn:
    :return:
    '''
    print('开始验证新链接的合法性')
    msg=os.urandom(32)
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)

def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('该链接不合法,关闭')
        conn.close()
        return
    print('链接合法,开始通信')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,bufsize,backlog=5):
    '''
    只处理链接
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新连接[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)
服务端
from socket import *
import hmac,os

secret_key=b'hello'
def conn_auth(conn):
    '''
    验证客户端到服务器的链接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
客户端

七、socketserver实现并发

我们都知道udp是无链接的,所以它本身就是并发。我们这主要都基于tcp的套接字实现并发,那么该如何实现并发?主要是基于tcp的套接字的两个循环,分别是链接循环、通信循环。

import socketserver

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)
                #发消息
                self.request.sendall(data.upper())
            except Exception as e:
                print(e)
                break

if __name__ == '__main__':
    s = socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer)   #ThreadingTCPServer是多线程
    # s = socketserver.ForkingTCPServer(('127.0.0.1',8080),MyServer)    #ForkingTCPServer是多进程
    s.serve_forever()
服务端
from socket import *
import subprocess
import struct
from functools import partial
ip_port = ('127.0.0.1',8080)
buffer_size = 1024

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd = input('>>:').strip()
    if not cmd:continue
    if cmd == 'quit':break
    tcp_client.send(cmd.encode('utf-8'))
    data = tcp_client.recv(buffer_size)
    print('收到服务端发来的消息:',data.decode('utf-8'))

tcp_client.close()
客服端

在这里客户端可以开多个,既能实现链接循环,也能实现通信循环。

posted @ 2019-09-11 22:36  流浪代码  阅读(228)  评论(0编辑  收藏  举报