python socket编程(tcp/udp)

晚点补充一下这篇文章。主要研究一下python下的tcp/udp的使用,包括收发双方;对于tcp的话要考虑循环接收。

一、socket基础

1、Socket API的接口

主要用到的socket接口就这些,关于其作用不在赘述(参数选项略作介绍)。

socket()
bind()
listen()
accept()
connect()
connect_ex()
send()
recv()
close()

关于socket函数这里多介绍一下。socket()函数用于创建socket对象,其源码如下:

    def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):
        # For user code address family and type values are IntEnum members, but
        # for the underlying _socket.socket they're just integers. The
        # constructor of _socket.socket converts the given argument to an
        # integer automatically.
        _socket.socket.__init__(self, family, type, proto, fileno)
        self._io_refs = 0
        self._closed = False

这里面有三个非常重要的参数分别是:family,type,proto;

(1)family:即协议域又称协议族。常用的有AF_INET、AF_INET6、AF_LOCAL(又称AF_UNIX)、AF_ROUTE等等。协议族决定了socket()的地址类型,在通信中必须采用对应的地址,AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

(2)type:指socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。

(3)proto:指定协议。常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

注意:上面的type和protocol并不是说可以随意组合。

其实。从使用的角度我们不用整的这么复杂,我们常用的无非以下几种:

family:
         socket.AF_INET:指定通过IPV4进行连接
         socket.AF_INET6:指定通过IPV6进行通信
type  :
         socket.SOCK_STREAM:通过TCP进行通信
         socket.SOCK_DGRAM: 通过udp进行通信

2、tcp交互服务端代码流程

import socket
 
#1、创建服务端的socket对象
listen_socket = socket.socket()
 
#2、绑定一个ip和端口
listen_socket.bind(('192.168.0.9',9091))
 
#3、服务器端一直监听是否有客户端进行连接
print('listen_socket is listening 9091')
listen_socket.listen()
 
#4、如果有客户端进行连接、则接受客户端的连接
conn_socket,addr =  listen_socket.accept()   #返回客户端socket通信对象和客户端的ip
 
#5、客户端与服务端进行通信
data = conn_socket.recv(1024).decode('utf-8')
print('服务端收到客户端发来的消息:%s'%(data))
 
#6、服务端给客户端回消息
conn_socket.send(b'I am a server')
 
#7、关闭socket对象
conn_socket.close()    #客户端对象
listen_socket.close()  #服务端对象

3、tcp交互客户端代码流程

import socket
#1、创建socket通信对象
conn_socket = socket.socket()
 
#2、使用正确的ip和端口去链接服务器
conn_socket.connect(('192.168.0.9',9091))
 
#3、客户端与服务器端进行通信
    # 给socket服务器发送信息
conn_socket.send(b'I am a client')
 
    # 接收服务器的响应(服务器回复的消息)
recvData = conn_socket.recv(1024).decode('utf-8')
print('客户端收到服务器回复的消息:%s'%(recvData))
 
#4、关闭socket对象
conn_socket.close()

注:配合流程图一起看。

python socket编程(tcp/udp)_第1张图片

左边表示服务器,右边则是客户端。

左上方开始,注意服务器创建「监听」Socket 的 API 调用:socket()、bind()、listen()、accept()。

「监听」Socket 做的事情就像它的名字一样。它会监听客户端的连接,当一个客户端连接进来的时候,服务器将调用 accept() 来「接受」或者「完成」此连接

客户端调用 connect() 方法来建立与服务器的链接,并开始三次握手。握手很重要是因为它保证了网络的通信的双方可以到达,也就是说客户端可以正常连接到服务器,反之亦然

上图中间部分往返部分表示客户端和服务器的数据交换过程,调用了 send() 和 recv()方法

下面部分,客户端和服务器调用 close() 方法来关闭各自的 socket

二、简单实例

1、简单tcp收发举例(python3)

服务端代码 

#!/usr/bin/python
#coding:utf-8

from socket import *
HOST = ''   #不指定的话就默认'localhost' 
PORT = 21568 #监<80>大长度
BUFSIZ = 1024  #指定接收TCP消息的最大长度
ADDR = (HOST,PORT) #

#listSock是服务端的监听socket
listSock = socket(AF_INET,SOCK_STREAM) #不指定参数默认为:AF_INET,SOCK_STREAM
listSock.bind(ADDR) #将主机号、端口号绑定到套接字
listSock.listen(5)  #开启TCP监听

while True:
    print('waiting for connection...')
    #connSock是连接socket 其实就是一个具体的socket连接.
    connSock, addr = listSock.accept()
    print('...connnecting from:', addr)

    while True:
        data = connSock.recv(BUFSIZ)
        print("recv:",data)
        if not data:
            break
        #prefix = b'reply:'
        rsp = data.upper()
        connSock.send(rsp)
        '''
        注意:下面打出来的并不一定是每次真正send回去的字符串,尤其是bufsize小于收发字符串大小的时候
        '''
        #print("send back:",rsp)
    connSock.close()
listSock.close()

客户端代码

from socket import *
 
HOST = '127.0.0.1' #or 'localhost'
PORT = 21568
BUFSIZ =1024
ADDR = (HOST,PORT)
 
conn_socket = socket(AF_INET,SOCK_STREAM)
conn_socket.connect(ADDR)
while True:
     data = input('input message:')
     if not data:
         break
     conn_socket.send(data.encode())
     data = conn_socket.recv(BUFSIZ)
     if not data:
         break
     print("recv:" + data.decode('utf-8'))
conn_socket.close()

2、看看buf过小(一次性接收不完所有消息)会发生什么情况。

首先把双方的bufsize都改成10,然后将服务端代码中的 “rsp = prefix + data.upper()” 语句 改成 “rsp =  data.upper()”,主要目的是为了更直观的看清收发机制。

如下左图为服务端日志、右侧为客户端日志。另外为了区分每次客户端发送msg和服务端的反映这里用了不同颜色的方框做了区分。这里重点分析一下前几波

1)当客户端发送“abcdefghijklmno”15个字符串给服务端的时候,服务端实际上是循环接受了两次;第一次接受了“abcdefghij”10个字符,第二次接受了剩下的“klmno”5个字符。按照代码程序知道每次接受完一波都会原模原样的转换大写然后返回。

也就是说:对于recv函数来讲还是要进行循环接受的。原因很简单,即使bufsize设的非常大待接收的文件依然有可能更大。

2)但是可以看到第一波客户端只收到了前10个字符。其原因也很简单,那就是客户端的recv函数没有进行循环接受,只有每次用户输入字符然后回车后才能接受一波数据,而且一次接受的大小也被限制为10字节。

3)所以当用户第二次输入数据“n”字串的时候客户端的返回了“KLMNON”。

python socket编程(tcp/udp)_第2张图片

注:关于上面讲到的这些涉及到tcp流式传输的灵魂。看下面这张图(对于发送方)。

python socket编程(tcp/udp)_第3张图片

(1)应用层:对应用层来说它只关心发送的数据DATA,应用调用send函数将数据拷贝至socket在内核中的发送缓冲区SO_SENFBUF即返回(注意数据不一定发送发送出去,send仅仅是拷贝数据至发送缓冲区),操作系统会将SO_SENDBUF中的数据取出来进行发送。

(2)传输层:传输层会在DATA前面加上TCP Header(20字节),构成一个完整的TCP报文段(Segement);

(3)网络层:网络层会在TCP报文段的基础上在添加一个IP Header,形成一个IP数据包(Packege);

(4)数据链路层:还会加上一个Datalink Header和CRC校验相关(14+4字节);

(5)物理层:还会加上SMAC(Source Machine,发送方的MAC地址),DMAC(Destination Machine,接受方的MAC地址)以及Type域加入。

同理对于接受方也一样:接收端也会有一个自己的接受buf。通信对方发来的所有的数据都堆积在这个接受buf中,应用层调用一次recv函数就顺序的从这个接受buf中取出相应size的数据。以此类推!!!

3、当数据过大时要怎么办?——循环接受

这里面做的事情就是说一次性接受完所有的数据然后在返回。

可以这么理解:send、recv的时候都是bytes(时刻考虑是否要encode一下),程序处理的时候最好是string。

服务端代码:

#!/usr/bin/python
#coding:utf-8

import socket
from time import time as now
import sys

HOST = ''   #不指定的话就默认'localhost' 
PORT = 21568 #监<80>大长度
BUFSIZ = 10  #指定接收TCP消息的最大长度
ADDR = (HOST,PORT) #

#listen_socket是服务端的监听socket
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #不指定参数默认为:AF_INET,SOCK_STREAM
listen_socket.bind(ADDR) #将主机号、端口号绑定到套接字
listen_socket.listen(5)  #开启TCP监听

print('waiting for connection...')
#conn_socket是连接socket 其实就是一个具体的socket连接.
conn_socket, addr = listen_socket.accept()
print('...connnecting from:', addr)

timeout = 20.0               # time to wait before reply
data_gap = 0.5              # should only be short time between data segments
conn_socket.settimeout(timeout)
data = ''
while 1:
    try:
        part = conn_socket.recv(BUFSIZ)
        print("part:",part)
        if not part:
            print("enter not part")  #正常接收下是没进到这个逻辑的
            after_recv = now()       #receive FIN from remote
            break
        data += part.decode()   #decode将bytes转换为str(bytes拼接可能有问题)
        conn_socket.settimeout(data_gap)
    except socket.timeout:      #这个超时时间是由前面"conn_socket.settimeout(data_gap)"来设置的
        if not data:
            print("No reply")
            sys.exit(1)
        else:                   #正常接收情况下走到了这里
            after_recv = now() - data_gap   # time out, assuming receive done
            print("enter else")
            break

print("recv:",data)
rsp = data.upper()
conn_socket.send(rsp.encode()) #要求参数必须是bytes,故encode一下
print("length:",len(rsp.encode()))
conn_socket.close()
listen_socket.close()

客户端程序:

#!/usr/bin/python
#coding:utf-8

import socket
from time import time as now
import sys


HOST = '127.0.0.1' #or 'localhost'
PORT = 21568
BUFSIZ =10
ADDR = (HOST,PORT)

conn_socket = socket.socket(socket.AF_INET ,socket.SOCK_STREAM)
conn_socket.connect(ADDR)
data = input('input message:')
if not data:
    sys.exit(1)
conn_socket.send(data.encode()) #
print("length:",len(data.encode()))


timeout = 20.0              # time to wait before reply
data_gap = 0.5              # should only be short time between data segments
conn_socket.settimeout(timeout)
data = ''
while 1:
    try:
        part = conn_socket.recv(BUFSIZ)
        print("part:",part)
        if not part:
            print("enter not part")  #这边是走到此逻辑才退出的(即使数据接受完了还是会多接受一次,然后就命中这里了)
            after_recv = now()       #receive FIN from remote
            break
        data += part.decode()   #decode将bytes转换为str(bytes拼接可能有问题)
        conn_socket.settimeout(data_gap)
    except socket.timeout:      #这个超时时间是由前面"conn_socket.settimeout(data_gap)"来设置的
        if not data:
            print("No reply")
            sys.exit(1)
        else:                   #
            after_recv = now() - data_gap   # time out, assuming receive done
            print("enter else")
            break

print("recv:",data)
conn_socket.close()

效果如下: 

python socket编程(tcp/udp)_第4张图片  python socket编程(tcp/udp)_第5张图片

疑问:有个疑问。为什么客户端在接受服务端返回的大写字串时明明数据已经接受完全了还要在接受一次呢??

答:这个应该是传说中的FIN字符导致的。例如说你不让服务端close(即人为的sleep一下在看看效果应该就知道了)

设置reuse、time.sleep()。

你可能感兴趣的:(python,socket,python,socket)