python网络编程-----socket

1.了解socket之前先回顾一下osi网络七层协议

每层常见的物理设备有: 

 

 2.tcp建立链接的过程,三次握手,四次挥手。

 

具体每一层的具体作用,参考链接:http://www.cnblogs.com/linhaifeng/articles/5937962.html

 

进入正题

3.什么是socket

socket是应用层与tcp/ip协议簇通信的中间软件抽象层,他是一组接口。

4.套接字

套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

基于文件类型的套接字家族

套接字家族的名字:AF_UNIX

unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

基于网络类型的套接字家族

套接字家族的名字:AF_INET

(还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我们只使用AF_INET)

 

5.套接字的工作流程:

可以借助一个生活中的例子去理解套接字的工作流程。

你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。

 6.socket()模块函数用法

 

 1 import socket
 2 socket.socket(socket_family,socket_type,protocal=0)
 3 socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0。
 4 
 5 获取tcp/ip套接字
 6 tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 7 
 8 获取udp/ip套接字
 9 udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
10 
11 由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
12 例如tcpSock = socket(AF_INET, SOCK_STREAM)

 

 

7。基于tcp的套接字

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

 

(1).基本版的通信:

   服务端:

import socket
#买手机
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#插卡
phone.bind(('127.0.0.1',8080))
#开机
phone.listen(5)
#等电话链接
print('server start...')
conn,client_addr=phone.accept() #(tcp链接,client_addr)
print('链接',conn)
print(client_addr)

#基于建立的链接,收发消息
client_data=conn.recv(1024)
print('客户端的消息',client_data)
conn.send(client_data.upper())

#挂电话链接
conn.close()

#关机
phone.close()

 

  客户端:

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))


phone.send('hello'.encode('utf-8'))
server_data=phone.recv(1024)
print('服务端回应的消息',server_data)

phone.close()

 

上述代码就是一个基本的模拟客户端跟服务端通信过程。

但是存在很多问题,比如,上述代码客户端建立连接发送一条消息就终止了,这显然没有什么意义。并且只能同一个客户端连接,连接断开了就终止了,其他客户端没办法再次建立连接了。

所以我们要加上连接循环跟通信循环解决上述两个问题:

 

(2)加上连接循环跟通讯循环:

  服务端:

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #解决地址占用的问题
phone.bind(('127.0.0.1',8080))
phone.listen(5)
print('server start...')
while True: #链接循环
    conn,client_addr=phone.accept()
    print(conn,client_addr)

    while True: #通讯循环
        try:
            client_data=conn.recv(1024)
            if not client_data:break #针对linux系统
            # print('has rev')
            conn.send(client_data.upper())
        except Exception: #针对windwos
            break
    conn.close()

phone.close()

 

 客户端:

import socket
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    phone.send(msg.encode('utf-8'))
    # print('====>has send')
    server_data=phone.recv(1024)
    # print('====>has recv')
    print(server_data.decode('utf-8'))

phone.close()

 

 

8.tcp的粘包现象:

只有TCP有粘包现象,UDP永远不会粘包

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

 

为什么粘包只有TCP有?

下面来看一下两个协议的差别

(1).TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
(2)UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
(3)tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头

 

TCP在什么情况下会产生粘包呢?

(1)发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

(2)接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

 

9.解决粘包现象的方法。

思路:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

代码:(下面是模拟了一个连接到服务器ssh远程执行命令的情形,来看怎么解决粘包现象)

服务端:

import socket
import struct  #这个模块的作用是可以把一个类型,如数字,转成固定长度的bytes
import subprocess
import json
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)
print('server start...')
while True: #链接循环
    conn,client_addr=phone.accept()
    print(conn,client_addr)

    while True: #通讯循环
        try:
            cmd=conn.recv(1024)
            if not cmd:break

            #执行命令,拿到结果
            res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)

            stdout=res.stdout.read()
            stderr=res.stderr.read()


            #制作报头
            header_dic={'total_size':len(stdout)+len(stderr),'md5':None}
            header_json=json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')


            #1 先发报头的长度(固定4个bytes)
            conn.send(struct.pack('i',len(header_bytes)))


            #2 先发报头
            conn.send(header_bytes)


            #3 再发真实的数据
            conn.send(stdout)
            conn.send(stderr)

        except Exception: #针对windwos
            break
    conn.close()

phone.close()

 

 

 

客户端:

import socket
import struct
import json
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080))

while True:
    cmd=input('>>: ').strip()
    if not cmd:continue
    #发命令
    phone.send(cmd.encode('utf-8'))


    #先收报头的长度
    struct_res=phone.recv(4)
    header_size=struct.unpack('i',struct_res)[0]

    #再收报头
    header_bytes=phone.recv(header_size)
    head_json=header_bytes.decode('utf-8')
    head_dic=json.loads(head_json)

    total_size=head_dic['total_size']
    #再收命令的执行结果
    recv_size=0
    data=b''
    while recv_size < total_size:
        recv_data=phone.recv(1024)
        recv_size+=len(recv_data)
        data+=recv_data

    #打印结果
    print(data.decode('gbk'))

phone.close()

 

posted @ 2017-08-24 21:54  嘟囔囔小孩  阅读(182)  评论(0编辑  收藏  举报