Python socket 基本传输实验 - Jacky Liu's Blog

Python socket 基本传输实验

Jacky Liu posted @ 2012年6月28日 12:59 in Python with tags python socket , 5815 阅读

    ---- 电脑互联以后的文件共享、samba 这些只是为了开发时的方便,系统运行时的内部信息传输还是靠底层的网络传输机制,或者说 socket。对我这样的网络白痴来说,一切得从零开始。幸好 Python 用起来很方便,短时间内(不考虑犯懒的因素)把 socket 摸索到实际能用的程度也不是幻想。

    ---- 下面一个测试程序算是个总结,除了演示 socket 基本的通信以外,也演示了客户端或者服务端挂掉以后会发生什么。先说一下两个基本考虑:
   
        第一,用 TCP socket 而不用 UDP socket。原因不总结了,我也不太懂,反正前一个用的最多。


        第二,用默认的 blocking socket,对每一个连接都开一个发送线程和一个接收线程,Windows 和 Linux 都是,不用 non-blocking socket + select() 的方式。(参考 Python 文档里的 "socket programming howto")。如果用后一种就要给 select() 函数设 timeout,不设肯定会吃掉 100% CPU,系统还有许多其它事要做,不能让网络传输占掉太多资源。但是设的话又不知设多少是好,如果要智能控制——有流量时加快,没流量时减慢,又太麻烦, 而且不一定好得过系统的线程调度。相比之下,前一种方式简单而且容易管理得多。(补记:这里想差了。如果把 select() 放进一个单独的线程里,那么 timeout 设多少可能是无关紧要的,但还是 blocking socket 比较简单。)

    ---- 测试程序:

 

# -*- encoding: utf-8 -*-



'''
实验平台:
	Ubuntu 12.04 LTS / Linux 3.2.0-25-generic-pae

实验目标:
	模拟客户端程序异常终止的情况,检视服务端 socket 会有什么样的行为

实验过程:
	1. 开一个监听进程,使用全局 server socket,执行无限循环监听指定端口。监听到连接后会开启一个接收线程和一个
	   发送线程对连接进行操作。
	2. 主进程开启一个客户线程模拟客户端,连上服务端 socket 以后进行一次发送和一次接收操作,然后在未关闭 socket
	   的情况下终止运行,模拟客户端异常终止的情况。
	3. 服务端的发送线程和接收线程分别终止以后,在主进程里关闭全局 server socket,使监听进程终止,整个实验程序
	   终止运行。

实验结果:
	客户端程序异常终止以后,服务端 socket 的 sendall() 操作会抛出异常导致发送线程终止,recv() 操作返回 b'',
	表示连接已损坏,导致接收线程终止。
'''

import socket
import multiprocessing
import time
import threading



__host_server__= 'localhost'
__port_server__= 9000

__host_client__= 'localhost'
__port_client__= 9001



#==================================== ↓ 以下是服务端 ↓ ====================================

# 全局 socket,最后在主进程内关闭。
sock_server= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_server.bind((__host_server__, __port_server__))
sock_server.listen(1)	# 开始监听



def _服务端监听进程():

	while True:	# 此循环只执行一次。
		try:
			conn, addr= sock_server.accept()
		except:
			print('_服务端监听进程() -- server socket 已关闭,监听进程退出 ...')
			break

		print('_服务端监听进程() -- 接收到来自 ' + str(addr) + ' 的连接。')

		发送线程= threading.Thread(target=_服务端发送线程, name='发送线程', kwargs={'conn':conn})
		发送线程.start()

		接收线程= threading.Thread(target=_服务端接收线程, name='接收线程', kwargs={'conn':conn})
		接收线程.start()



def _服务端发送线程(conn):
	
	__msg__= b'0123456789'
	count= 0

	while True:
		try:
			conn.sendall(__msg__)
		except:
			print('_服务端发送线程() -- 连接已损坏,发送线程终止。')
			break
		count += 1
		print('_服务端发送线程() -- 已发 ' + str(count) + ' 条。')
		time.sleep(0.1)		# 间隔 0.1 秒发送



def _服务端接收线程(conn):
	
	while True:
		data= conn.recv(8192)

		if data:
			print('_服务端接收线程() -- 接收到客户端信息: ' + data.decode('utf-8'))
		else:
			print('_服务端接收线程() -- 连接已损坏,接收线程终止。')
			break
	try:
		conn.shutdown(socket.SHUT_RDWR)
	except:
		pass

	conn.close()



#==================================== ↓ 以下是客户端 ↓ ====================================

def _客户端线程():
	'''

	'''
	sock_client= socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	sock_client.bind( (__host_client__, __port_client__) )
	sock_client.connect( (__host_server__, __port_server__) )

	print('_客户端线程() -- 客户端已连接,开始发送 ...')
	sock_client.sendall(b'Hello, World !')

	data= sock_client.recv(8192)
	print('_客户端线程() -- 接收到服务端发来的数据: ' + data.decode('utf-8'))

	# XXX: 模拟客户端挂掉,无预警退出
	print('_客户端线程() -- 本客户端非正常退出 ...')



#==================================== ↓ 以下是主进程 ↓ ====================================

# 启动监听进程
监听进程= multiprocessing.Process(name='监听进程', target=_服务端监听进程)
监听进程.start()

time.sleep(1)

客户线程= threading.Thread(target=_客户端线程, name='客户线程')
客户线程.start()

# 关闭监听进程,不然整个程序没法退出。
time.sleep(1)
sock_server.shutdown(socket.SHUT_RDWR)
sock_server.close()


    ---- 下面是运行结果:

        _客户端线程() -- 客户端已连接,开始发送 ...
        _客户端线程() -- 接收到服务端发来的数据: "0123456789"
        _客户端线程() -- 本客户端非正常退出 ...
        _服务端监听进程() -- 接收到来自 ('127.0.0.1', 9001) 的连接。
        _服务端接收线程() -- 接收到客户端信息: "Hello, World !"
        _服务端接收线程() -- 连接已损坏,接收线程终止。
        _服务端发送线程() -- 已发 1 条。
        _服务端发送线程() -- 连接已损坏,发送线程终止。
        _服务端监听进程() -- server socket 已关闭,监听进程退出 ...

 

 

 

Avatar_small
依云 说:
2012年6月28日 16:12

「如果用后一种就要给 select() 函数设 timeout,不设肯定会吃掉 100% CPU」——不是的,不设则永不超时。

我不知道你为什么要弄两个线程而不是两个不同的进程来分别作服务端和客户端,这在实际编程中几乎是不可能遇到的(都是同一进程内了,交换数据何必多此一举呢)。

另外不建议使用中文标识符。

Avatar_small
Jacky Liu 说:
2012年6月28日 18:11

@依云: 那个应该确实是在不同的进程里,因为服务端的发送线程和接收线程都是从监听进程里起的,而监听进程是通过 multiprocessing.Process() 运行的。

关于 select() 和 socket 的工作模式,我的了解少的很。刚才又查一下文档,原来 select() 和 socket 都有 timeout,这个有点绕人。我写的时候想的是这种情况:把 socket 设成 setblocking(False),然后 select() 的 timeout 参数设成 0 而不是省略,这样应该会吃掉 100% CPU,实际没法工作。

另一种相反的情况,把 socket 设成 setblocking(True) 然后 select() 的 timeout 参数省略掉,应该能正常工作没问题,只是要有一些异常处理来对付某个连接挂掉的情况,不能影响其它连接,这也是我觉得复杂难弄的地方。

而其它的情况,也就是 socket 和 select() 一个阻塞而另一个不阻塞,在实际当中应该是没意义的吧?请问你用 socket 时是怎么用的?

另外,每个连接单起收发线程 跟 一个线程里用 select() 处理所有连接,你觉得哪一种比较好?我是菜鸟所以倾向于前面比较简单的那个。

Avatar_small
依云 说:
2012年6月28日 18:38

@Jacky Liu: 设成 0 和不设是不同的。前者就是轮询了,而后者是不超时。这个和 socket 是不是阻塞的没关系。

I/O 的多路复用和异步是挺复杂的,好在已经有封装好的库了。标准库里有 asyncore 和 asynchat,我通常用 tornado。不过简单的情况下自己 select 下也挺方便的。

初学的话当然先用简单的阻塞式了,你要用多线程的话也行,不过程序复杂一点可能就会出各种问题了。

要看我怎么用的,到我的 github 上的仓库里 grep 吧,主要在 winterpy 里,另外 rpysh 里用了「祼的」socket。

Avatar_small
Jacky Liu 说:
2012年6月29日 19:48

@依云: 我昨天又想了一下,然后又搜了点资料。我想 asyncore 和多线程的区别本质应该是这样的:假设 asyncore 需要对付 10 个连接,把 asyncore.loop() 放到一个线程里,再假设另外还有 3 个工作线程,那么在某一个时段里每个连接得到执行的机会都是 1/4。如果用多线程,每个连接一个线程,那同样的时段里每个连接得到执行的机会只有 1/13。当然用 select() + non-blocking socket 的方法跟 asyncore(或者其它基于 non-blocking socket 的库)应该没本质区别。

Avatar_small
Jacky Liu 说:
2012年6月29日 20:14

@依云: 资料总结一下,有以下几个印象,没办法这方面经验太浅了:

1. asyncore 的文档开头那一段。说 asyncore 本质上是实现并行的一种技术,跟多线程一样。而 asyncore 是为 IO 设计的,我的理解是处理 IO 应该比多线程更有效率。这一段里又说,只有 IO 密集的任务才应该用 asyncore,CPU 密集的任务应该用 pre-emptive thread。

2. 又去搜关于 python 线程的资料,印象是 python 线程确实是 pre-emptive 的,但是也有人不同意这一点。总而言之,应该不会有某一个 python 线程太久不被执行的情况。

3. “socket programming howto” 这一篇,结尾那一段里作者说他在 Windows 上都是用多线程,而且效果很好。他没说是不是用 non-blocking socket,我的理解是没用。而且他暗示这样做是因为性能的关系。他说使用哪种方式跟平台也有很大关系。

我想如果用多线程的方式,怕的应该是以下的情况:如果其中一个连接经常会有一大坨信息要送(正是我的情况),而处理这个连接的线程在优先级上得不到照顾,那这一大坨信息可能会延宕好久才能传送完。

我眼下的情况是两台电脑互联,只有一个连接要对付。但是我想写成可扩展的,到时就会有多个连接。所以我正在考虑用 non-blocking socket 的方式,只是不知道这样对 CPU 的压力有多大。如果太大也不划算。

另外我想知道,你说用多线程的话在复杂情况下可能出问题,具体指的是什么?

Avatar_small
依云 说:
2012年6月29日 22:34

1. pre-emptive thread 是什么啊?依我看 CPU 密集的任务应该不用 Python。
3. Windows 比较喜欢多线程,性能也好。而 Windows 上最好的异步机制 IOCP 貌似没多少语言支持。

「应该不会有某一个 python 线程太久不被执行的情况」——会的,我就遇到过,lxml 直接从网络取数据的时候,有一个等待网络的阶段拿着 GIL(全局解释器锁)不放,所以那时只有一个线程能够执行(所以 UI 经常卡那么一下……)

处理得当的话,事件驱动 I/O 效率是非常好的(想想 nginx 和 tornado),同时对付成百上千连接(特别是长连接)毫无压力。

多线程最大的问题是数据共享和线程同步。共享数据访问时要加锁。如果一些线程等待另外的线程完成某件事,则需要进行各种通知。弄不好就会发生死锁。

Python 的多线程的另外一个问题就是 GIL 了,也就是同一时间只有一个线程能够执行 Python 指令。

Avatar_small
依云 说:
2012年6月29日 22:37

反正我现在是尽量避免多线程。异步实在不行画个状态转移图就搞定了,多线程写一个线程的时候要小心翼翼地想着要不要加锁,什么时候才能加锁,搞不好死锁了调试起来麻烦死。

Avatar_small
Jacky Liu 说:
2012年6月29日 23:10

@依云: pre-emptive thread 在这篇里有讲:Introduction to Threads Programming with Python,作者是 Norman Matloff。我的印象是 pre-emptive thread 是指何时切换由单独的调度(比如 GIL)来决定而不是线程自己决定,这样一般能保证每个线程都有比较均衡的执行机会。但是这个帖子里有人不同意:http://bugs.python.org/issue1432694

因为一个消息在最后一字节收完之前都不能用,所以我最关心的是,如果某个连接有一大坨数据(比如一次几 Mb)过来时必须要尽快处理。我在想着能不能写个测试程序比较一下性能。有结果我再贴上来。

Avatar_small
Jacky Liu 说:
2012年6月29日 23:35

@依云: 另,你所说的连 GIL 也会被无视的情况我以前不知道。多谢。

Avatar_small
依云 说:
2012年6月29日 23:51

@Jacky Liu: 「一个消息在最后一字节收完之前都不能用」——不是的啊,收到多少数据就返回多少数据的,只有有没有数据可读的时候才会阻塞的。

pre-emptive thread 原来是抢占式线程啊……

Avatar_small
Jacky Liu 说:
2012年7月03日 10:10

@依云: 我是说,比如发的是一个 3M 大的 pickle 文件或者图片压缩文件,那必须等传完了才能用。另,原来那个叫抢占式线程啊,我读的不是计算机:)

asynchat 我已经试通了,我贴上来 。。。

Avatar_small
safa 说:
2014年2月23日 09:58

那边copy来的错的;socket资源锁住了

Avatar_small
safa 说:
2014年2月23日 09:58

那边copy来的错的;socket资源锁住了

Avatar_small
lory 说:
2014年5月01日 18:01

你这个中文起的函数名,怎么运行 啊

Avatar_small
Jacky Liu 说:
2014年5月01日 23:17

@lory: 烧高香一柱,香灰和水吞服即可


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter
Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee