1

I've started to modify an example made in python to stream the output of a counter by means of a TCP server. Below the code

import socket
import sys
import time
from thread import *

HOST = ''   # Symbolic name meaning all available interfaces
PORT = 8888 # Arbitrary non-privileged port

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1)
print 'Socket created'

#Bind socket to local host and port
try:
    s.bind((HOST, PORT))
except socket.error as msg:
    print 'Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
    sys.exit()

print 'Socket bind complete'

#Start listening on socket
s.listen(10)
print 'Socket now listening'

#Function for handling connections. This will be used to create threads
def clientthread(conn):
    #Sending message to connected client
    #conn.send('Welcome to the server. Type something and hit enter\n') #send only takes string     
    #infinite loop so that function do not terminate and thread do not end.
    count = 0
    while True:
        count = count + 1
        #Receiving from client
        #data = conn.recv(1024)
        #reply = 'OK...' + data
        #if not data: 
        #    break
        reply = str(count)+'\n'
        print reply
        conn.send(reply)
        time.sleep(1)
    #came out of loop
    conn.close()

#now keep talking with the client
while 1:
    #wait to accept a connection - blocking call
    conn, addr = s.accept()
    print 'Connected with ' + addr[0] + ':' + str(addr[1])

    #start new thread takes 1st argument as a function name to be run, second is the tuple of arguments to the function.
    start_new_thread(clientthread ,(conn,))

s.close()

I want to get the counter form a remote http client using my browser. I've to wait the counter reaches at least 260 counts before seeing it on the browser. After the 1st burst of 260 counts everything runs synchronized at server and client side. I've made different attempts reducing the sent buffer size but every time there is a big lag at the beginning.

mx0
  • 6,445
  • 12
  • 49
  • 54
dzuliani
  • 111
  • 1
  • 1
  • 8

2 Answers2

0

Browsers expect one big response by default and don't render until they have a minimum amount of data: The following link discusses this limitation in Chunked Encoding, a way to send data in chunks.

using-transfer-encoding-chunked-how-much-data-must-be-sent-before-browsers-s/16909228#16909228

So this limitation will apply even if you cobble together a valid HTTP1.0 chunked response series : The browser has valid headers, and will accept your data in chunks, but will still delay rendering anything until a bunch have passed.

The question is: do you really need a full web browser as a client? There are simpler ways to read some raw data from a TCP stream.. You could write one in python, or use e.g. netcat or even an old telnet client? Problem solved ;-)

Okay, say that it really needs to be a browser.. Than you'll have to do much more work. One standard (W3C) way of sending out live data immediately, is a mechanism called Server-Sent-Events. You sent the Content-Type text/event-stream, and then data, line by line, preceded by "data: ":

def clientthread(conn):
    conn.send("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\n");
    count = 0
    while True:
        count = count + 1
        conn.send("data: %d\r\n" % (count))
        print count
        time.sleep(1)
    conn.close()

There are libraries to do it idiomatically, currently, etc.. but I wanted to show how simple it basically is.

.. but now, you need some an EventSource in client-side Javascript to make sense of this to e.g. set some HTML element to the counter value each time a new counter is received.

It doesn't stop there.. You now have to serve the resulting HTML and script, and if it isn't on the same server, make sure to set various security-related headers or your browser will ignore your scripts..

Also, things may get complex soon, unless this is an academic exercise, you will have to consider robustness, standards compliance, edge cases, etc.. I would strongly recommend using a higher-level HTTP server implementation like HTTPServer and BaseHTTPRequestHandler that do much of that kind of work for you.

This example (python3) serves both the html (at /) with example EventSource and the SSE stream (at /counter) with the counter:

import sys, time                                                                                                                                                                                                                                
from http.server import HTTPServer,BaseHTTPRequestHandler                                                                                                                                                                                       
from socketserver import ThreadingMixIn                                                                                                                                                                                                         
from socket import error                                                                                                                                                                                                                        

html_and_js = """<html>                                                                                                                                                                                                                         
<head>                                                                                                                                                                                                                                          
    <meta charset="UTF-8">                                                                                                                                                                                                                      
    <title>Counter SSE Client</title>                                                                                                                                                                                                           
</head>                                                                                                                                                                                                                                         
<body>                                                                                                                                                                                                                                          
Count:<span id="counter">0</span>                                                                                                                                                                                                               
<script>                                                                                                                                                                                                                                        
    "use strict";                                                                                                                                                                                                                               
    var counter = document.getElementById('counter');                                                                                                                                                                                           
    var event_source=new EventSource("/counter");                                                                                                                                                                                               
    event_source.onmessage=function(msg) {                                                                                                                                                                                                      
        counter.innerHTML=msg.data;                                                                                                                                                                                                             
    };                                                                                                                                                                                                                                          
    </script>                                                                                                                                                                                                                                   
</body>                                                                                                                                                                                                                                         
</html>                                                                                                                                                                                                                                         
"""                                                                                                                                                                                                                                             

class SSECounterRequestHandler(BaseHTTPRequestHandler):                                                                                                                                                                                         

    server_version = "DzulianisCounter/0.1"                                                                                                                                                                                                     

    def do_html(self):                                                                                                                                                                                                                          
        self.send_header("Content-type", "text/html")                                                                                                                                                                                           
        self.send_header("Access-Control-Allow-Origin", "*")                                                                                                                                                                                    
        self.end_headers()                                                                                                                                                                                                                      
        self.wfile.write(bytes(html_and_js,'UTF-8'))                                                                                                                                                                                            

    def do_sse(self):                                                                                                                                                                                                                           
        self.counter=0                                                                                                                                                                                                                          
        self.send_header("Content-type", "text/event-stream")                                                                                                                                                                                   
        self.send_header("Cache-Control", "no-cache")                                                                                                                                                                                           
        self.end_headers()                                                                                                                                                                                                                      

        self.running=True                                                                                                                                                                                                                       
        while self.running:                                                                                                                                                                                                                     
            try:                                                                                                                                                                                                                                
                self.wfile.write(bytes('data: %d\r\n\r\n' % (self.counter),'UTF-8'))                                                                                                                                                            
                self.counter+=1                                                                                                                                                                                                                 
                time.sleep(1)                                                                                                                                                                                                                   
            except error:                                                                                                                                                                                                                       
                self.running=False                                                                                                                                                                                                              

    def do_GET(self):                                                                                                                                                                                                                           
        self.send_response(200)                                                                                                                                                                                                                 
        if self.path=='/counter':                                                                                                                                                                                                               
            self.do_sse()                                                                                                                                                                                                                       
        else:                                                                                                                                                                                                                                   
            self.do_html()                                                                                                                                                                                                                      

class SSECounterServer(ThreadingMixIn, HTTPServer):                                                                                                                                                                                             
        def __init__(self,listen):                                                                                                                                                                                                              
                HTTPServer.__init__(self,listen,SSECounterRequestHandler)                                                                                                                                                                       

if __name__=='__main__':                                                                                                                                                                                                                        
    if len(sys.argv)==1:                                                                                                                                                                                                                        
            listen_addr=''                                                                                                                                                                                                                      
            listen_port=8888                                                                                                                                                                                                                    
    elif len(sys.argv)==3:                                                                                                                                                                                                                      
            listen_addr=sys.argv[1]                                                                                                                                                                                                             
            listen_port=int(sys.argv[2])                                                                                                                                                                                                        
    else:                                                                                                                                                                                                                                       
            print("Usage: dzulianiscounter.py [<listen_addr> <listen_port>]")                                                                                                                                                                    
            sys.exit(-1)                                                                                                                                                                                                                        

    server=SSECounterServer((listen_addr,listen_port))                                                                                                                                                                                          
    server.serve_forever()       

This is much more efficient, and has a better response time, than e.g. having the page poll some URL regularly, or shudders reloading the page all the time :-) At your speed (1 per second) this also keeps the http connection open, avoiding connection overhead but adding some memory overhead to OS's the network stack, which could be felt if you would get many simultaneous users on that.

Enjoy!

  • Ok, if I use socat (a some sort of netcat) it works great as you told. I've also tried the again with a browser but using the Content-Type text/event-stream method inside the python script, but unfortunately the browser now ask me to open or save a file. Why? – dzuliani Oct 06 '18 at 22:59
  • Because it can't interpret the meaning of your data, it'll try to save it to a file. SSE is a *protocol*, not a content-type.The browser has no way of knowing what the data that you're pushing towards it represents. That's why it needs the client-side code (between the `` tags near the top of my complete example to make sense of it, which in this case, is updating a counter inside the page, corresponding to the counter inside your python code. –  Oct 07 '18 at 08:08
  • Ok, now is more clear to me but, I've a program (not written by me, it's compiled) that acts as a TCP server and it's able to stream data which is imediately interpreted by the browser (no intial delay at all) and the client-side code is not needed at all. – dzuliani Oct 07 '18 at 09:57
  • Do you know how it works? (try capturing the HTTP traffic) –  Oct 07 '18 at 13:14
  • Ok, I've taken a look at the software, it yelds a long string at every count (140 chars = 140 bytes) so everytime I start the browser I've to wait just 8 counts (1120 bytes) which is just above the 1st big response of 1024 bytes for my browser(firefox). So to me it was almost Instantaneous but it is not. Currently I'm starting the server side with a 1st response to a client made of 1024 blanks. Not so elgant but it works. – dzuliani Oct 07 '18 at 16:20
  • I would think that is very wasteful of bandwidth, indeed not a very elegant solution. Not going to upvote that, but happy that your issue is solved for now. –  Oct 07 '18 at 17:48
  • I know not elegat but working ;-) Thanks for the support. – dzuliani Oct 07 '18 at 20:18
0

I have Created A Threaded HTTP Server where latency can be given as input

from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading , sys
from time import sleep

server_ip = '0.0.0.0'
server_port = int(sys.argv[1])
latency_time = float(sys.argv[2])


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-Type',
                         'text/plain; charset=utf-8')
        sleep(latency_time)
        self.end_headers()
        message = threading.currentThread().getName() + ",  Latency time = " + str(latency_time)
        self.wfile.write(message.encode('utf-8'))
        self.wfile.write(b'\n')


class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""


if __name__ == '__main__':
    server = ThreadedHTTPServer((server_ip, server_port), Handler)
    print('Starting server, use <Ctrl-C> to stop')
    server.serve_forever()

this is how i would start the server

$ python3 HTTPLatencyServer.py 3389 .3
Starting server, use <Ctrl-C> to stop
127.0.0.1 - - [29/Apr/2022 11:31:33] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [29/Apr/2022 11:31:33] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [29/Apr/2022 11:31:34] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [29/Apr/2022 11:31:34] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [29/Apr/2022 11:31:34] "GET / HTTP/1.1" 200 -

sample Output

$ curl localhost:3389                  
Thread-101,  Latency time = 0.3