JWebServer/Simple
Building a J Web Server
J supports socket programming, but does not currently come with a web server. Let's rectify that.
For a first draft, let's write a HTTP/1.0 server which is designed for low-volume GET requests. This server won't support HTML forms with the method=POST attribute, nor will it be particularly useful under high load. The point of this server is to illustrate the basic concepts. Later one we can write a more robust server -- one which supports POST, and which perhaps supports HTTP/1.1.
Also, for simplicity, I'm going to assume that the J program which acts as a web server will only be servicing HTTP requests. I'll indicate in the code where this assumption is made, in case someone wants to do something more general.
These systems will use global variables exclusively so that it's easier to examine the internals of the program.
If you want more background on how socket programming works, please complete J's "Socket Driver Server" lab.
webserver0=: verb define 80 webserver0 y. NB. try to use port 80 by default : port=: x. require 'socket' coinsert 'jsocket' sdclose ; sdcheck sdgetsockets '' server=: {. ; sdcheck sdsocket '' sdcheck sdbind server; AF_INET; ''; port sdcheck sdlisten server, 1 while. 1 do. while. server e. ready=: >{. sdcheck sdselect (sdcheck sdgetsockets ''),'';'';<1e3 do. sdcheck sdaccept server end. for_socket. ready do. request=: ; sdcheck sdrecv socket, 65536 0 sdcheck (socket responseFor request) sdsend socket, 0 sdcheck sdclose socket end. end. ) responseFor=: dyad define 'HTTP/1.0 200 OK',CRLF,'Content-Type: text/plain',CRLF,CRLF,y. )
This web server simply sends the client's request back to the client with a minimal set of http headers.
If you run webserver0, it will should "hang" and if you use a web server to visit http://localhost you should get back a page showing a browser request. Note that the right argument to webserver0 is ignored and the optional left argument is the port to use. If you run this on a machine with another web server, you'll need to specify an alternate port. For example:
8080 webserver0 '' NB. urls of the form http://localhost:8080/...
To stop this server, you'll need to either (a) use the break key, or (b) kill the J program.
Exercises for the reader:
1. Try requesting alternate urls, such as http://localhost/example.htm. Can you see how the url becomes a part of the request?
1. do something more interesting with the url (other than simply returning
the response).
Weaknesses:
This implementation does not deal with buffering at all. Thus, it won't always do the right thing. When receiving request it might respond before the full request has been received. You can see this if you telnet to port 80 and type in a request by hand. When sending a response it might have to wait on the client. This is hard to observe on small responses but with very large responses you can see this by arranging for the client to not read the request from the server.
This implementation assumes that it owns all sockets. This means that attempting to use sockets for any other purpose (client or server) will not work properly.
Also, client requests won't queue (which will show up under heavy load). To make client requests queue (instead of forcing clients to retry while other clients are accessing the system) replace the 1 in the line sdcheck sdlisten server, 1 with the number of clients that should be allowed to wait in queue. This is probably less important than the buffering issues.
This implementation doesn't support HTTP/1.1. This is not a real weakness, but this will be discussed in more detail later (and, perhaps, support will be implemented for HTTP/1.1).
Support should be included for operations which are typical of web servers. For example: parsing of values from html forms. (And, more generally, parsing the request and generating the responses.)
Here's a version which addresses one of these problems -- buffering of input.
webserver1=: verb define 80 webserver1 y. NB. try to use port 80 by default : port=: x. require 'socket' coinsert 'jsocket' sdclose ; sdcheck sdgetsockets '' server=: {. ; sdcheck sdsocket '' sdcheck sdbind server; AF_INET; ''; port sdcheck sdlisten server, 5 ('ina',":server)=: httpAccept while. 1 do. cleanup=:'' in=. >{. sdcheck sdselect (sdcheck sdgetsockets ''),'';'';<1e3 for_socket. in do. ('ina',":socket)~ socket end. erase"0 cleanup end. ) erase=: 4!:55 responseFor=: dyad define 'HTTP/1.0 200 OK',CRLF,'Content-Type: text/plain',CRLF,CRLF,y. ) httpAccept=: verb define socket=. >{. sdcheck sdaccept server ('ina',":socket)=: httpRecv ('inb',":socket)=: '' ) httpRecv=: verb define request=. ('inb',":y.)=:(".'inb',":y.),; sdcheck sdrecv y., 65536 0 if. 1 e. (CRLF,CRLF) E. request do. sdcheck (y. responseFor request) sdsend y., 0 sdcheck sdclose y. cleanup=: cleanup, (;:'ina inb') ,L:0 ": y. end. )
Notes:
- other protocols can be supported, in addition to http, if they can be
made to have their receive function be named inasocketnumber.
- Only input is buffered here, which means this is still a light duty
web server. If the output is small, the underlying protocol layers will buffer it, however if it is large the server might wind up waiting for the client to receive the bulk of the response.
- Deleting the ina function and the inb buffer is deferred till after the ina function has exited (otherwise trying to delete it would cause an error).
- webserver1 does not support HTTP 0.9 -- any such requests will cause the client to wait indefinitely (eventually timing out). The earlier implementation didn't have a problem with this because it simply used the initial content of the tcp stream (typically one packet) regardless of whether that was a complete request or not.
- webserver1 still does not support gathering HTTP POST requests (though it might, coincidentally, capture part or all of the body of such requests).
Next: event driven code