問題描述:
平板的串口連接超級網口(超級網口:理解為串口跟網口的映射),python端通過socket庫來讀取數據,當我實例化兩個socket client來讀同一個超級網口的時候,其中一個Client概率性出現無法讀取數據的現象。
連接關系如下圖所示:平板將串口連到超級網口的串口端,超級網口在接收到串口數據之后立馬通過TCP Server將數據發送給所有連接的客戶端,我們當前的場景下是實例化了兩個client,會出現其中某個串口偶爾讀不到數據的現象。
超級網口硬件連接圖如下:
超級網口配置信息:
- K2 在TCP Server 模式下也有KeepAlive 功能用于實時監測連接的完整。
- 通常用于局域網內與TCP 客戶端的通信。適合于局域網內沒有服務器并且有多臺電腦或是手機向服務器請求數據的場景。同TCP Client 一樣有連接和斷開的區別,以保證數據的可靠交換。
- 本模式支持有人自主的同步波特率功能(RFC2217)功能
- 在TCP Server 模式下,K2 主動監聽設置的本機端口,有連接請求時響應并創建連接,當K2 的串口收到數據后,同時發送(也就是說是串口有數據就主動發送給連接的客戶,)給所有與該K2 服務器建立連接的設備。如果跨公網訪問K2 的TCP Server,需要在路由器上做端口映射。
- K2 做TCP Server 的情況下,最多可以接受16 個Client 連接(連接數可自定義),本地端口號為固定值,不可設置為0。
- K2 做TCP Server,當連接Client 數量超過設定最大值時,默認新連接踢掉舊連接,可通過網頁修改此功
能。
- TCP Server 模式下,連接Client 的數量可在1 到16 個之間任意設置,---默認4 個---,已連接Client的IP 可在內置網頁狀態界面顯示,按連接計算發送/接收數據。
- TCP Server 模式下,當連接數量達到最大值時,新連接是否踢掉舊連接可設置
原因:
由于我們的平臺框架占用了一個client名額、虛擬串口占用一個名額、我的用例里面再占用一個名額,X區域(網頁標簽打開串口:websocket方式)占用了一個名額、因此四個名額(就是上面說的默認四個,但是我實際上也將這個值修改為16后測試同樣也是只有四個client是同步的)被占用完了,我用例里面就實時讀取不到數據,因為我的TCP Server允許接16個但并不代表我有同步發送16個client的能力,從實際效果(網頁標簽打開client方式)來看,僅有三個(虛擬串口占用一個)標簽即client是同步收到數據的,其他client雖然偶爾能收到數據但是可以明顯看到中間書有數據缺失的,因此要么改寫框架的串口讀寫方式要么限制client數量。
即:由于超級網口廠商自身產品的缺陷導致的軟件使用BUG,結案。
一下是搜尋問題過程中的一些學習,留作記錄。
問題分解實時思路:
目前我還不知道socket的一些基礎知識:
看完這三篇博客之后,有了個大概的認識,即我服務器端(超級網口內部實現)是接收到串口數據之后就把數據發送給當前所有連接的client,而我們client端只要不顯示地關閉連接就不會斷開連接的,因此基本可以確定是我在寫代碼的時候兩個client的(IP,port)是一樣的,所以一個client在后臺一直輪訓取數據,而我的程序里面新建同樣的socket去讀數據,正常情況下應該是報錯才對(這里我理解錯了,socket中端口號存在的意義是實現不同電腦間進程之間的同步的,是一一對應的;每建立一個socket,client就會產生一個新的port編號,而我們的服務器端(超級網口)是同時最多發送給16個client的(根據當前連接的client來統一發送的),所以應該都會接收到數據才對),但是實際上卻沒報錯,偶爾還能正常工作,因此我們去查看下系統的源碼:
Socket Client的read實現方式如下:
def read(self, size=-1):
# Use max, disallow tiny reads in a loop as they are very inefficient.
# We never leave read() with any leftover data from a new recv() call
# in our internal buffer.
rbufsize = max(self._rbufsize, self.default_bufsize)
# Our use of StringIO rather than lists of string objects returned by
# recv() minimizes memory usage and fragmentation that occurs when
# rbufsize is large compared to the typical return value of recv().
buf = self._rbuf
buf.seek(0, 2) # seek end
if size < 0:
# Read until EOF
self._rbuf = StringIO() # reset _rbuf. we consume it via buf.
while True:
try:
data = self._sock.recv(rbufsize)
except error, e:
if e.args[0] == EINTR:
continue
raise
if not data:
break
buf.write(data)
return buf.getvalue()
else:
# Read until size bytes or EOF seen, whichever comes first
buf_len = buf.tell()
if buf_len >= size:
# Already have size bytes in our buffer? Extract and return.
buf.seek(0)
rv = buf.read(size)
self._rbuf = StringIO()
self._rbuf.write(buf.read())
return rv
self._rbuf = StringIO() # reset _rbuf. we consume it via buf.
while True:
left = size - buf_len
# recv() will malloc the amount of memory given as its
# parameter even though it often returns much less data
# than that. The returned data string is short lived
# as we copy it into a StringIO and free it. This avoids
# fragmentation issues on many platforms.
try:
data = self._sock.recv(left)
except error, e:
if e.args[0] == EINTR:
continue
raise
if not data:
break
n = len(data)
if n == size and not buf_len:
# Shortcut. Avoid buffer data copies when:
# - We have no data in our buffer.
# AND
# - Our call to recv returned exactly the
# number of bytes we were asked to read.
return data
if n == left:
buf.write(data)
del data # explicit free
break
assert n <= left, "recv(%d) returned %d bytes" % (left, n)
buf.write(data)
buf_len += n
del data # explicit free
#assert buf_len == buf.tell()
return buf.getvalue()
其中默認情況下的是把緩沖區所有數據都讀出來:
buf = self._rbuf
buf.seek(0, 2) # seek end
if size < 0:
# Read until EOF
self._rbuf = StringIO() # reset _rbuf. we consume it via buf.
while True:
try:
data = self._sock.recv(rbufsize)
except error, e:
if e.args[0] == EINTR:
continue
raise
if not data:
break
buf.write(data)
return buf.getvalue()
self._rbuf在init()時候初始化成: self._rbuf = StringIO()
因此首先得弄清楚StringIO()是什么:可以粗略理解為就是一個存在于內存中的文件,操作她跟操作普通文件一樣。
StringIO:(可以參考這)
StringIO的行為與file對象非常像,但它不是磁盤上文件,而是一個內存里的“文件”。
我們可以像操作磁盤文件那樣來操作StringIO。
就是生成一個StringIO對象,維護一個緩沖區,你可以像操作文件一樣操作它。
cStringIO:
Python標準模塊中還提供了一個cStringIO模塊,它的行為與StringIO基本一致,但運行效率方面比StringIO更好。
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
因為這個是python的一個內置庫,所以我們可以去python官網看一下介紹:(網址在這)
翻譯:這個模塊以一種file-like的類來實現的,這個類讀寫字符串buffer或者叫內存文件。
class StringIO.StringIO([buffer])
:
當一個StringIO對象被創建的時候,能通過賦值一個字符串來初始化。如何沒傳字符串的話文件位置為0;
疑問:這個內存文件是多長呢?還是說是可以變長的?那總得有個限制把?
現在我們回到上面:
當size<0,也就是默認情況下讀,此時初始化的self._rbuf已經備份到buf了,我們將self._rbuf重新開一個內存文件(StringIO()),相當于復位;然后開一個死循環,嘗試去recv(rbufsize)數據,假如出現異常了分兩種情況,一種是繼續(操作被系統中斷了,因此重新來)另一種是直接拋異常(其他異常不允許出現)
其中EINTR源碼中是這樣定義的:EINTR = getattr(errno, 'EINTR', 4),也就是說從errno文件中去看是否有EINTR屬性,沒有的話就返回默認值4,有則返回對應值。
假如返回的數據是空,那么我直接跳出循環,把buf里面殘留(假如上次我只讀了1個字符,這次讀全部,那么就有可能了~)的數據發出去就行了,假如有數據的話我將它拼接到buf中,然后一起發回去。
那么什么是EINTR異常呢?簡單來說就是:如果read()讀到數據為0,那么就表示文件讀完了,如果在讀的過程中遇到了中斷則read()應該返回-1,同時置errno為EINTR。
慢系統調用(slow system call):此術語適用于那些可能永遠阻塞的系統調用。永遠阻塞的系統調用是指調用有可能永遠無法返回,多數網絡支持函數都屬于這一類。如:若沒有客戶連接到服務器上,那么服務器的accept調用就沒有返回的保證。
EINTR錯誤的產生:當阻塞于某個慢系統調用的一個進程捕獲某個信號且相應信號處理函數返回時,該系統調用可能返回一個EINTR錯誤。例如:在socket服務器端,設置了信號捕獲機制,有子進程,當在父進程阻塞于慢系統調用時由父進程捕獲到了一個有效信號時,內核會致使accept返回一個EINTR錯誤(被中斷的系統調用)。當碰到EINTR錯誤的時候,可以采取有一些可以重啟的系統調用要進行重啟,而對于有一些系統調用是不能夠重啟的。例如:accept、read、write、select、和open之類的函數來說,是可以進行重啟的。不過對于套接字編程中的connect函數我們是不能重啟的,若connect函數返回一個EINTR錯誤的時候,我們不能再次調用它,否則將立即返回一個錯誤。針對connect不能重啟的處理方法是,必須調用select來等待連接完成。
總結:其實這里就是實現了一個上層協議,用來處理數據長度不可控時的buffersize定義問題。默認情況下我們先把上次可能沒讀完的(因為上次可能是先讀一部分指定長度的數據呀,比如上次我就讀了1個字符,那么極有可能這次我讀的時候還在_rbuf中殘留了一些數據,因此寫代碼的時候要考慮這個進去)數據先放到一個臨時buf中,然后再直接recv()去讀一次數據,最多讀rbufsize個數據,最少讀0個,把數據讀回來之后跟上次殘留的一起拼接好然后返回給用戶,這也就在read不指定長度的情況下把所有緩沖區的數據讀出來的情況。而有指定長度的話基本原理還是差不多,只是檢查一下長度再返回特定長度數據并做好數據維護。同時其他地方你也可以參考這個流程來做同樣的上層協議。我認為官方的這個流程很是規范。
self._sock.recv(rbufsize)這個函數再啰嗦一句:它是馬上返回的,假如是阻塞模式那么有個超時參數,非阻塞模式下假如一下沒讀到數據那么是會拋異常的(由于這個異常可控且已知所以可以選擇性忽略掉的),如下面的就是我們自己寫的一個socket程序,仿照socket.read()寫的,可以發現都是基于recv()函數來的,然后再在其上做一些檢測工作。
def read(self, bufsize=2048, timeout=0, is_blocked=False, print_log=False):
data = ''
self.client.setblocking(is_blocked)
if print_log:
logger.info('++++++++read serail[%s] start++++++++' % str(self.address))
if timeout:
start_time = time.time()
end_time = time.time() + timeout
while time.time() <= end_time:
try:
data = data + self.client.recv(bufsize)
except socket.error, e:
if (str(e).find('10035') != -1 or str(e).find('11')) and is_blocked == False:
# if socket client is not blocked, then client recv nothing from server, it will raise
# socker error, in windows, the error code is 10035, in linux, the error code is 11.
pass
else:
logger.error('dut read fail[%s]' % str(e))
traceback.print_exc()
self.connect()
if time.time() > end_time:
break
else:
data = data + self.client.recv(bufsize)
if print_log:
if data:
logger.debug(data)
logger.info(
'--------read serial[%s] finish--------' % str(self.address))
self.client.setblocking(True)
return data
stackoverflow上有關于recv()的討論,摘抄如下:
socket.recv(*bufsize*[, *flags*])
Receive data from the socket. The return value is a string representing the data received.
The maximum amount of data to be received at once is specifiedby *bufsize*.
See the Unix manual page *recv(2)* for the meaning ofthe optional argument *flags*;
it defaults to zero.
Note:
For best match with hardware and network realities,
the value of *bufsize*should be a relatively small power of 2, for example, 4096.
- The bufsize param for the recv(bufsize) method is not optional. You'll get an error if you call recv() (without the param).
- The bufferlen in recv(bufsize) is a maximum size. The recv will happily return fewer bytes if there are fewer available.
這里可以知道兩條信息:
- bufsize參數是必須的,不是可選的;
- buffersize是一個最大值,如果沒有足夠的數據也是會及時返回噠~
疑問:那么當是阻塞模式的時候呢?
But now you have a new problem: how do you know when the sender has sent you a complete message?
The answer is: you don't. You're going to have to make the length of the message an explicit part of your protocol.
Here's the best way: prefix every message with a length, either as a fixed-size integer (converted to network byte order using socket.ntohs() or socket.ntohl() please!) or as a string followed by some delimiter (like '123:'). This second approach often less efficient, but it's easier in Python.
Once you've added that to** your protocol**, you need to change your code to handle recv() returning arbitrary amounts of data at any time. Here's an example of how to do this. I tried writing it as pseudo-code, or with comments to tell you what to do, but it wasn't very clear. So I've written it explicitly using the length prefix as a string of digits terminated by a colon. Here you go:
length = None
buffer = ""
while True:
data += self.request.recv()
if not data:
break
buffer += data
while True:
if length is None:
if ':' not in buffer:
break
# remove the length bytes from the front of buffer
# leave any remaining bytes in the buffer!
length_str, ignored, buffer = buffer.partition(':')
length = int(length_str)
if len(buffer) < length:
break
# split off the full message from the remaining bytes
# leave any remaining bytes in the buffer!
message = buffer[:length]
buffer = buffer[length:]
length = None
# PROCESS MESSAGE HERE
上面的意思是,由于發過來的數據長度我們無法預估,因此需要我們上層自己定義協議(也就是方法)來處理數據的接收流程,上述代碼就是一種處理方法。
_socket.py:socket是導入了這個庫的,這個庫負責定義好了接口,并對接口進行描述,如下所示
def recv(self, buffersize, flags=None): # real signature unknown; restored from __doc__
"""
recv(buffersize[, flags]) -> data
Receive up to buffersize bytes from the socket. For the optional flags
argument, see the Unix manual.
When no data is available, block until
at least one byte is available or until the remote end is closed. When
the remote end is closed and all data is read, return the empty string.
"""
pass
關于socket阻塞問題:
阻塞模式下是緩沖區沒數據就等,一有數據(盡管沒有buffersize個數據)我就返回。
非阻塞模式下我來的時候緩沖區剛好有數據,拿了就返回,假如沒有數據的話,我就報警了啊不我就報異常了噢~
作者:靈劍
鏈接:https://www.zhihu.com/question/51834325/answer/127694264
來源:知乎 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
socket分為阻塞和非阻塞兩種,可以通過setsockopt,或者更簡單的setblocking, settimeout設置。
阻塞式的socket的recv服從這樣的規則:
當緩沖區內有數據時,立即返回所有的數據;當緩沖區內無數據時,阻塞直到緩沖區中有數據。
非阻塞式的socket的recv服從的規則則是:
當緩沖區內有數據時,立即返回所有的數據;當緩沖區內無數據時,產生EAGAIN的錯誤并返回(在Python中會拋出一個異常)。
兩種情況都不會返回空字符串,返回空數據的結果是對方關閉了連接之后才會出現的。
由于TCP的socket是一個流,因此是不存在“讀完了對方發送來的數據”這件事的。
你必須要每次讀到數據之后,根據數據本身來判斷當前需要等待的數據是否已經全部收到,來判斷是否進行下一個recv。
可以看一下hiredis庫的接口設計,hiredis中的Reader有兩個接口,分別是feed和gets,feed每次送入一部分數據,不需要保證是正確分片的;
gets則返回已經得到的完整的結果,如果返回False,表示已經沒有新的結果。
基本上所有的TCP的socket編程都是遵循這樣的方法:讀入新數據;判斷有沒有完整的新消息;處理新消息,或者等待更多數據。
題主:
一般的實現判斷的方法有下面幾種:
1.自定義協議的分界符,比如回車換行。
2.第一個字段給出長度,然后是數據,讀的時候先拿到長度,然后讀取那么多就好了
3.固定長度。
socket網絡編程:
這個博主講到了socket編程的方方面面,講了web通信的前后端知識以及基本實現代碼,很好理解。
關于socket.recv的精彩論述:
作者的一些理解,很有共鳴。