Day9 - Python基础9 socket基础、粘包

本节内容:

1.socket的介绍

2.基于tcp的socket

3.基于tcp的问题分析

4.基于udp的socket

5.基于udp的问题分析

6.基于udp的ntp服务

7.基于tcp的远程执行命令服务

8.粘包

9.粘包的两种解决办法

 

1.socket的介绍

socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求

注意的一点:socket 的应用程序在内存的用户空间中,而操作系统在内存空间中,socket的应用程序要想使用网卡(硬件) 必须通过操作系统间接去内存的缓冲区去收发数据。也就是说socket的  收 --》去缓冲区拿  发---》送到缓冲区,跟socket有关的就是缓冲区了。

 

2.基于tcp的socket

####server端######

ss = socket() #创建服务器套接字
ss.bind()      #把地址绑定到套接字
ss.listen()      #监听链接
inf_loop:      #服务器无限循环
    cs = ss.accept() #接受客户端链接
    comm_loop:         #通讯循环
        cs.recv()/cs.send() #对话(接收与发送)
    cs.close()    #关闭客户端套接字
ss.close()        #关闭服务器套接字(可选)


###client端#####
cs = socket()    # 创建客户套接字
cs.connect()    # 尝试连接服务器
comm_loop:        # 通讯循环
    cs.send()/cs.recv()    # 对话(发送/接收)
cs.close()            # 关闭客户套接字

3.tcp的一些问题分析

0:实现循环收发数据  (采用循环)
 
1:客户输了一个 回车 导致 都阻塞在recv阶段
分析:
    收发都是去内存的缓存区去取数据,而你发送了空 取不到数据呀,自然就服务端就阻塞在了recv,
    而对于客户端他发送了空数据后,下一步就接受来自服务端的数据。而你的服务端还没收到数据,
    怎么给你客户端发数据,自然也阻塞住了。
解决办法:加上一个if 条件判断,当用户输入为空时,continue
 
2:客户端的强制关闭 导致服务端发生错误
分析:
    我服务端recv得到的是data 和conn 你把conn干掉,服务端的recv肯定报错呀
解决办法:try  except  当出现错误,就break退出收发循环,并关闭客户端的链接

3:客户端正常关闭close,导致了服务端一直循环  
	解决办法:服务端加个if not data:break 退出通信循环
     
4:现在的服务器只能接收一个accept 我要接收多个客户端。
解决办法:while true  循环接收accept 接收到的链接都被挂起来了。

4:有的同学在重启服务端时可能会遇到

这个是由于你的服务端仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手 2.syn洪水攻击 3.服务器高并发情况下会有大量的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))
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf

编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
 
然后执行 /sbin/sysctl -p 让参数生效。
 
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;

net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
方法二

4.基于UDP的socket

######udp服务端#######

ss = socket()   #创建一个服务器的套接字
ss.bind()       #绑定服务器套接字
inf_loop:       #服务器无限循环
     cs = ss.recvfrom()/ss.sendto() # 对话(接收与发送)
ss.close()   


########udp客户端########

cs = socket()   # 创建客户套接字
comm_loop:      # 通讯循环
    cs.sendto()/cs.recvfrom()   # 对话(发送/接收)
cs.close()                      # 关闭客户套接字

通过以上代码:我们可以看出服务端少了一个listen的过程,那么分析下为什么需要listen?

因为tcp是基于一个双向链接的协议,当多个客户端来之后 只会为其中的一个客户端提供服务,其他的客户端都处于链接挂起的状态。那udp没有链接啊,自然就没有listen 和 accept的过程。

5.udp套接字的一些问题分析:

1:接收和发送问题  
收recvfrom()  (收到的是一个元祖的格式,第一个是发送过来的数据 第二个又是个客户端的套接字元祖 ); 
发sendto     (数据,服务端的套接字); [因为你udp没有链接了呀,所以每次发送数据需要带上服务端的套接字]


2:实现自由收发信息 (while循环)

3:udp 发送回车,可以接受到数据;(recv默认从缓存区拿不到数据就阻塞住,而recvfrom默认可以从缓存区拿到空数据)  问题保留?等会解答

4:tcp和udp一个服务端,对应多个客户端,udp可以实现并发
  因为他本根不就需要建立连接。

6.基于udp的ntp服务实现 

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

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


while True:
    print("服务端开启..")
    data,addr = sk_server.recvfrom(buffer_size)
    print("客户端的地址:",addr)


    if not data:
        back_data = time.strftime('%Y-%m-%d %H:%M:%S').encode('utf-8')
    else:
        data = str(data,'utf-8')
        print("收到服务的命令:", data)
        back_data = time.strftime(data).encode('utf-8')
    sk_server.sendto(back_data,addr)
ntp_server
import  socket
ip_port  = ('127.0.0.1',9009)
cs = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)


while True:
    input_data = input('请输入你的数据:').encode('utf-8')
    if input_data == 'quit':break
    cs.sendto(input_data,ip_port)

    data,addr = cs.recvfrom(1024)
    data = data.decode('utf-8')
    print("服务端返回的命令:",data)
ntp_client
客户端的输出:
请输入你的数据:
服务端返回的命令: 2018-03-13 11:04:02
请输入你的数据:%Y
服务端返回的命令: 2018
请输入你的数据:%Y-%m
服务端返回的命令: 2018-03
请输入你的数据:

7.基于tcp的远程执行命令服务

from socket import  *
import  subprocess
import  struct
ip_port = ('127.0.0.1',9014)
buffer_size = 1024

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

while True:
    print("服务端开启..")
    conn,addr=sk_server.accept()
    print("客户端的地址:",addr)
    while True:
        try:
            data = conn.recv(buffer_size).decode('utf-8')
            print("收到服务的命令:",data)

            back_cmd = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,stdin=subprocess.PIPE)

            err_cmd = back_cmd.stderr.read()

            if not err_cmd:      ##如果命令执行成功
                cmd_back = back_cmd.stdout.read()
                if not cmd_back:  ##如果是执行命令cd .. 默认,客户端从缓冲区是拿不到数据的,给个默认的返回
                    cmd_back = '执行成功'.encode('gbk')
            else:
                cmd_back = back_cmd.stderr.read()


            ###直接发送 默认会两个一起发送,不过长度固定占4个字节
            conn.send(struct.pack('i',len(cmd_back)))
            conn.send(cmd_back)

        except Exception as e :
                print(e);break
                conn.close()
xshell_server
import  socket
import  struct
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

sk.connect(('127.0.0.1',9014))

while True:
    input_data = input('请输入你的数据:')
    if not input_data:continue
    if input_data == 'quit':break

    ##发送命令
    sk.send(input_data.encode('utf-8'))

    ##接收固定占的长度4个字节
    cmd_len  = sk.recv(4)
    cmd_int_len = struct.unpack('i',cmd_len)[0]

    recv_size = 0
    recv_msg = b''
    while recv_size < cmd_int_len:
        recv_msg += sk.recv(1024)
        recv_size = len(recv_msg)

    print("服务端返回的命令:",recv_msg.decode('gbk'))
xshell_client

8.粘包

udp是不存在粘包的,那么粘包的产生是因为:

1:我们客户端定义了recv(1024) 默认接收1024个字节,
2:而服务端一次发送大于1024字节的数据 到客户端的缓冲区,
3:客户端从缓冲区一次只取1024字节,剩下的数据就只能留着下次再去,
4:这就是所谓的粘包

粘包的进一步产生原因分析:

粘包1:TCP有网络延迟,采用使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
	
粘包2:数据量发送的大,接收的少,再接收的话就是上次未接收完的数据。

为什么udp不存在粘包?

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

而udp是一个消息一个消息的发送,一次recvfrom 可以接收到数据加ip、端口的元组,
那么我只要找到ip_and_port的位置 到下一个消息ip_and_port ,中间的位置都是需要的数据,
这样我就知道了我一次需要提取多少个字节。因为消息跟消息之间存在了界限

9.解决粘包 

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

low版本的解决方法

服务端                    					客户端 
发要要发送的数据长度	  					接收要接收数据的长度 
接收一个ready【主要避免TCP的Nagle算法】		                发送一个数据  ready  
发送 要发送的数据 							while 判断,接收数据 
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()
server
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'))
client

 

为何low:

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

前戏:将命令的长度 转为固定的4个字节

关于:struct 模块的使用 移步:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html

>>> import subprocess
>>> cmd_b = subprocess.Popen('ipconfig',shell=True,stdout=subprocess.PIPE,stderr
=subprocess.PIPE)
>>> data = cmd_b.stdout.read()
>>> len(data)    	##获取执行命令结果的长度是多少
1799
>>> cmd_len = struct.pack('i',len(data))  ##将命令的长度转为对应的占4个字节
>>> cmd_len
b'\x07\x07\x00\x00'
>>> struct.unpack('i',cmd_len)	
(1799,)
>>> struct.unpack('i',cmd_len)[0]		##将4个字节,转为int长度 
1799
>>> len(data)
1799
>>>

实现:

from socket import  *
import  subprocess
import  struct
ip_port = ('127.0.0.1',9014)
buffer_size = 1024

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

while True:
    print("服务端开启..")
    conn,addr=sk_server.accept()
    print("客户端的地址:",addr)
    while True:
        try:
            data = conn.recv(buffer_size).decode('utf-8')
            print("收到服务的命令:",data)

            back_cmd = subprocess.Popen(data,shell=True,stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,stdin=subprocess.PIPE)

            err_cmd = back_cmd.stderr.read()

            if not err_cmd:      ##如果命令执行成功
                cmd_back = back_cmd.stdout.read()
                if not cmd_back:  ##如果是执行命令cd .. 默认,客户端从缓冲区是拿不到数据的,给个默认的返回
                    cmd_back = '执行成功'.encode('gbk')
            else:
                cmd_back = back_cmd.stderr.read()


            ###直接发送 默认会两个一起发送,不过长度固定占4个字节
            conn.send(struct.pack('i',len(cmd_back)))
            conn.send(cmd_back)

        except Exception as e :
                print(e);break
                conn.close()
server
 1 import  socket
 2 import  struct
 3 sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 4 
 5 sk.connect(('127.0.0.1',9014))
 6 
 7 while True:
 8     input_data = input('请输入你的数据:')
 9     if not input_data:continue
10     if input_data == 'quit':break
11 
12     ##发送命令
13     sk.send(input_data.encode('utf-8'))
14 
15     ##接收固定占的长度4个字节
16     cmd_len  = sk.recv(4)
17     cmd_int_len = struct.unpack('i',cmd_len)[0]
18 
19     recv_size = 0
20     recv_msg = b''
21     while recv_size < cmd_int_len:
22         recv_msg += sk.recv(1024)
23         recv_size = len(recv_msg)
24 
25     print("服务端返回的命令:",recv_msg.decode('gbk'))
client

 

 

  

posted @ 2018-03-17 01:28  你是我的神奇  阅读(192)  评论(0编辑  收藏  举报