socket网络编程

TCP编程:

Socket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”,而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

大多数连接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。

先来看看客户端怎么创建一个基于TCP连接的Socket:

# 导入socket库:
import socket

# 创建一个socket:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('www.sina.com.cn', 80))  #Web服务的标准端口都是80,所以这里设置80端口

创建Socket时,AF_INET指定使用IPv4协议,如果要用更先进的IPv6,就指定为AF_INET6SOCK_STREAM指定使用面向流的TCP协议,这样,一个Socket对象就创建成功,但是还没有建立连接。

注意参数是一个tuple,包含地址和端口号。

建立TCP连接后,我们就可以向新浪服务器发送请求,要求返回首页的内容:

# 发送数据:
s.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')

TCP连接创建的是双向通道,双方都可以同时给对方发数据。但是谁先发谁后发,怎么协调,要根据具体的协议来决定。例如,HTTP协议规定客户端必须先发请求给服务器,服务器收到后才发数据给客户端。

发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收新浪服务器返回的数据了:

# 接收数据:
buffer = []
while True:
    # 每次最多接收1k字节:
    d = s.recv(1024)
    if d:
        buffer.append(d)
    else:
        break
data = b''.join(buffer)

接收数据时,调用recv(max)方法,一次最多接收指定的字节数,因此,在一个while循环中反复接收,直到recv()返回空数据,表示接收完毕,退出循环。

当我们接收完数据后,调用close()方法关闭Socket,这样,一次完整的网络通信就结束了:

# 关闭连接:
s.close()

接收到的数据包括HTTP头和网页本身,我们只需要把HTTP头和网页分离一下,把HTTP头打印出来,网页内容保存到文件:

header, html = data.split(b'\r\n\r\n', 1)
print(header.decode('utf-8'))
# 把接收的数据写入文件:
with open('sina.html', 'wb') as f:
    f.write(html)

现在,只需要在浏览器中打开这个sina.html文件,就可以看到新浪的首页了。

参考,廖雪峰Python:http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001432004374523e495f640612f4b08975398796939ec3c000

 

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

客户端套接字函数
s.connect() 主动初始化TCP服务器连接
s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数
s.recv() 接收TCP数据
s.send() 发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)
s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
s.recvfrom() 接收UDP数据
s.sendto() 发送UDP数据
s.getpeername() 连接到当前套接字的远端的地址
s.getsockname() 当前套接字的地址
s.getsockopt() 返回指定套接字的参数
s.setsockopt() 设置指定套接字的参数
s.close() 关闭套接字

面向锁的套接字方法
s.setblocking() 设置套接字的阻塞与非阻塞模式
s.settimeout() 设置阻塞套接字操作的超时时间
s.gettimeout() 得到阻塞套接字操作的超时时间

面向文件的套接字的函数
s.fileno() 套接字的文件描述符
s.makefile() 创建一个与该套接字相关的文件

粘包:
当我们基于socket传输数据时,可能会发生粘包。粘包问题主要是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

两种情况下会发生粘包。


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

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

下面将结合客户端(windows)访问服务端(Linux)达成类似ssh协议的效果,在客户端上输入指令,访问服务端并执行指令。并且解决粘包的问题,上代码。
服务端:
#coding:utf-8
#买手机
import socket   #导入必备的模块
import struct       #打包与解包
import json         #格式化,类似eval
import subprocess    #模块用于启动一个新的进程并且与之通信
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#创建一个socket,AF_INET指定使用IPv4协议,如果要用IPv6,就指定为AF_INET6
#SOCK_STREAM指定使用面向流的TCP协议
#绑定电话卡
ip_port = ('192.168.20.128',8081)  #建立连接,大于1024的端口可以任意使用
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #加入一条socket配置,重用ip和端口,防止四次挥手time_wait状态在占用地址
phone.bind(ip_port)  #监听端口
#开机
phone.listen(5)    #调用listen()方法开始监听端口,传入的参数指定等待连接的最大数量
#等待电话

#链接循环
while True:  #服务器程序通过一个永久循环来接受来自客户端的连接,accept()会等待并返回一个客户端的连接:
    conn,addr = phone.accept()      #接受一个新连接
    print('client addr',addr)  #验证已经连接上
    #通讯循环
    while True:
        try:
            cmd = conn.recv(1024)  #每次最多接收1K字节
            res = subprocess.Popen(cmd.decode('utf-8'),  #使用Popen来创建进程,并与进程进行复杂的交互
                                   shell=True, #表示程序由shell执行
                                   stdout=subprocess.PIPE, #输出
                                   stderr=subprocess.PIPE)  #错误输出 PIPE表示管道接收
            out_res = res.stdout.read()  #定义一个接收管道输出的变量
            err_res = res.stderr.read()  #定义一个接收错误输出的变量
            data_size = len(out_res)+len(err_res)  #获取发送的数据所占大小
            head_dic = {'data_size':data_size}      #数据的大小可以有无限大,所以很难定义struct的方法,可以是'i'可以是'q',所以这里将数据大小以字典的形式封装,然后将字典作为报头发送
            head_json = json.dumps(head_dic)     #将python字典类型解析为json数据
            head_bytes = head_json.encode("utf-8")      #将数据以utf8编码

            #part1:先发报头的长度
            head_len = len(head_bytes)          #获取报头长度
            conn.send(struct.pack('i',head_len))        #字典长度struct方式打包并发送到客户端
            #part2:再发送报头
            conn.send(head_bytes)           #此时客户端已经知道报头长度,发送报头不会产生粘包
            #part3:最后发送数据
            conn.send(out_res)
            conn.send(err_res)

        except Exception:  #如果客户端挂断通讯服务端会抛出异常,这里加入万能异常处理让服务端在客户端挂断后能正常运行
            break

    conn.close()
phone.close()
View Code

客户端:

#买手机
import socket
import struct
import json

phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
#拨通电话
ip_port = ('192.168.20.128',8081)           #连接服务端的IP与端口
phone.connect(ip_port)

#通信循环
while True:
    # 发消息
    cmd = input('>>>:').strip()
    if not cmd: continue        #如果发空,则继续输入
    phone.send(bytes(cmd,encoding='utf8'))      #客户端发送指令

    #part1:先收报头的长度
    head_struct = phone.recv(4)         #我们自定了服务端发送报头的struct方式是'i'格式,所以这里接收4个字节则是完整的报头
    head_len = struct.unpack('i',head_struct)[0]        #解包,返回一个元组,第一个元素即是报头的长度,即字典长度

    #part2:再收报头
    head_bytes = phone.recv(head_len)       #规定接收到报头长度的数据,即接收到了完整的字典
    head_json = head_bytes.decode('utf-8')  #收到的数据进行解码并交给变量

    head_dic = json.loads(head_json)    #同样的对dumps了的数据用loads方法解析为Python格式的数据
    print(head_dic)
    data_size = head_dic['data_size']      #拿到了字典,再取出对应key里的真正需要的数据的长度值

    #part3:收数据
    recv_size = 0       #先定义一个变量,比对接收的数据
    recv_data = b''     #初始化一个空bytes
    while recv_size<data_size:      #比对数据大小,循环接收数据直到接收到的数据大小等于服务端返回的数据大小
        data = phone.recv(1024)     #每次接收1k字节
        recv_size+=len(data)    #接收总数据循环加收到的实际数据来比对
        recv_data+=data         #将收到的数据拼接

    print(recv_data.decode('utf-8'))    #解码数据并打印
phone.close()
View Code

效果:

 

 

这里会用到新的2个模块。

struct:     http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html

subprocess:    http://blog.csdn.net/imzoer/article/details/8678029

 
posted @ 2017-05-03 16:27  Mitsuis  阅读(196)  评论(0编辑  收藏  举报