|
2 | 2 | ;; Manoel Vilela |
3 | 3 |
|
4 | 4 | (defpackage :lisp-chat-server |
5 | | - (:use :usocket :cl :lisp-chat-config) |
| 5 | + (:use :usocket :cl :lisp-chat-config :sb-thread) |
6 | 6 | (:export :main)) |
7 | 7 |
|
8 | 8 | (in-package :lisp-chat-server) |
9 | 9 |
|
10 | | -;; constants |
11 | | -;; (don't use defconstant because create a bug as ASDF system compile time) |
12 | | -(defparameter +commands-names+ '("/users" "/help" "/log" "/quit" "/uptime")) |
13 | | -(defparameter +day-names+ '("Monday" "Tuesday" "Wednesday" |
14 | | - "Thursday" "Friday" "Saturday" "Sunday")) |
15 | | -(defparameter +uptime+ (multiple-value-list (get-decoded-time))) |
16 | 10 |
|
17 | 11 | ;; global vars |
| 12 | +(defvar *day-names* '("Monday" "Tuesday" "Wednesday" |
| 13 | + "Thursday" "Friday" "Saturday" "Sunday")) |
| 14 | +(defvar *uptime* (multiple-value-list (get-decoded-time))) |
| 15 | +(defparameter *commands-names* '("/users" "/help" "/log" "/quit" "/uptime")) |
18 | 16 | (defparameter *clients* nil) |
19 | 17 | (defparameter *messages-stack* nil) |
20 | 18 | (defparameter *messages-log* nil) |
21 | | - |
| 19 | +(defparameter *server-nickname* "@server") |
22 | 20 |
|
23 | 21 | ;; thread control |
24 | | -(defvar *message-semaphore* (sb-thread:make-semaphore :name "message semaphore" |
| 22 | +(defvar *message-semaphore* (make-semaphore :name "message semaphore" |
25 | 23 | :count 0)) |
26 | | -(defvar *client-mutex* (sb-thread:make-mutex :name "client list mutex")) |
| 24 | +(defvar *client-mutex* (make-mutex :name "client list mutex")) |
| 25 | + |
27 | 26 |
|
28 | 27 |
|
29 | 28 | (defstruct message |
30 | | - from content time) |
| 29 | + "This structure abstract the type message with is saved |
| 30 | + into *messages-log* and until consumed, temporally pushed |
| 31 | + to *messages-stack*. FROM, CONTENT and TIME has type string" |
| 32 | + from |
| 33 | + content |
| 34 | + time ) |
31 | 35 |
|
32 | 36 | (defstruct client |
33 | | - name socket address) |
| 37 | + "This structure handle the creation/control of the clients of the server. |
| 38 | + NAME is a string. Socket is a USOCKET:SOCKET and address is a ipv4 encoded |
| 39 | + string. " |
| 40 | + name |
| 41 | + socket |
| 42 | + address) |
34 | 43 |
|
35 | 44 |
|
36 | 45 | (defun socket-peer-address (socket) |
| 46 | + "Given a USOCKET:SOCKET instance return a ipv4 encoded IP string" |
37 | 47 | (format nil "~{~a~^.~}\:~a" |
38 | 48 | (map 'list #'identity (get-peer-address socket)) |
39 | 49 | (get-peer-port socket))) |
40 | 50 |
|
41 | 51 | (defun client-stream (c) |
| 52 | + "Select the stream IO from the client" |
42 | 53 | (socket-stream (client-socket c))) |
43 | 54 |
|
44 | 55 |
|
45 | 56 | (defun debug-format (&rest args) |
| 57 | + "If *debug* from lisp-chat-config is true, print debug info on |
| 58 | + running based on ARGS" |
46 | 59 | (if *debug* |
47 | 60 | (apply #'format args))) |
48 | 61 |
|
49 | 62 |
|
50 | 63 | (defun get-time () |
| 64 | + "Return a encoded string as HH:MM:SS based on the current timestamp." |
51 | 65 | (multiple-value-bind (second minute hour) |
52 | 66 | (get-decoded-time) |
53 | 67 | (format nil "~2,'0d:~2,'0d:~2,'0d" hour minute second))) |
54 | 68 |
|
55 | 69 |
|
56 | 70 | (defun formated-message (message) |
| 71 | + "The default message format of this server. MESSAGE is a string |
| 72 | + Changing this reflects all the layout from client/server. |
| 73 | + Probably this would be the MFRP: Manoel Fucking Raw Protocol. |
| 74 | + Because this we can still use netcat as client for lisp-chat." |
57 | 75 | (format nil "|~a| [~a]: ~a" |
58 | 76 | (message-time message) |
59 | 77 | (message-from message) |
60 | 78 | (message-content message))) |
61 | 79 |
|
62 | 80 | (defun command-message (content) |
63 | | - (let* ((from "@server") |
| 81 | + "This function prepare the CONTENT as a message by the @server" |
| 82 | + (let* ((from *server-nickname*) |
64 | 83 | (time (get-time)) |
65 | 84 | (message (make-message :from from :content content :time time))) |
66 | 85 | (formated-message message))) |
67 | 86 |
|
68 | 87 | (defun call-command-by-name (string) |
| 88 | + "Wow, this is a horrible hack to get a string as symbol for functions/command |
| 89 | + like /help /users /log and so on." |
69 | 90 | (funcall (find-symbol (string-upcase string) :lisp-chat-server))) |
70 | 91 |
|
71 | 92 | ;; user commands prefixed with / |
72 | 93 | (defun /users () |
| 94 | + "Return a list separated by commas of the currently logged users" |
73 | 95 | (command-message (format nil "~{~a~^, ~}" (mapcar #'client-name *clients*)))) |
74 | 96 |
|
75 | 97 |
|
76 | 98 | (defun /help () |
77 | | - (command-message (format nil "~{~a~^, ~}" +commands-names+))) |
| 99 | + "Show a list of the available commands of lisp-chat" |
| 100 | + (command-message (format nil "~{~a~^, ~}" *commands-names*))) |
78 | 101 |
|
79 | 102 |
|
80 | 103 | (defun /log (&optional (depth 20)) |
| 104 | + "Show the last messages typed on the server. |
| 105 | + DEPTH is optional number of messages frames from log" |
81 | 106 | (format nil "~{~a~^~%~}" (reverse (subseq *messages-log* 0 |
82 | 107 | (min depth (length *messages-log*)))))) |
83 | 108 |
|
84 | 109 | (defun /uptime () |
| 110 | + "Return a string nice encoded to preset the uptime since the server started." |
85 | 111 | (multiple-value-bind |
86 | 112 | (second minute hour date month year day-of-week dst-p tz) |
87 | | - (values-list +uptime+) |
| 113 | + (values-list *uptime*) |
88 | 114 | (declare (ignore dst-p)) |
89 | 115 | (command-message |
90 | 116 | (format nil |
91 | 117 | "Server online since ~2,'0d:~2,'0d:~2,'0d of ~a, ~2,'0d/~2,'0d/~d (GMT~@d)" |
92 | | - hour |
93 | | - minute |
94 | | - second |
95 | | - (nth day-of-week +day-names+) |
96 | | - month |
97 | | - date |
98 | | - year |
| 118 | + hour minute second |
| 119 | + (nth day-of-week *day-names*) |
| 120 | + month date year |
99 | 121 | (- tz))))) |
100 | 122 |
|
101 | 123 |
|
102 | 124 | (defun push-message (from content) |
| 125 | + "Push a messaged FROM as CONTENT into the *messages-stack*" |
103 | 126 | (push (make-message :from from |
104 | 127 | :content content |
105 | 128 | :time (get-time)) |
106 | 129 | *messages-stack*) |
107 | | - (sb-thread:signal-semaphore *message-semaphore*)) |
| 130 | + (signal-semaphore *message-semaphore*)) |
108 | 131 |
|
109 | 132 | (defun client-delete (client) |
110 | | - (sb-thread:with-mutex (*client-mutex*) |
| 133 | + "Delete a CLIENT from the list *clients*" |
| 134 | + (with-mutex (*client-mutex*) |
111 | 135 | (setf *clients* (remove-if (lambda (c) |
112 | 136 | (equal (client-address c) |
113 | 137 | (client-address client))) |
|
120 | 144 | (socket-close (client-socket client))) |
121 | 145 |
|
122 | 146 | (defun send-message (client message) |
| 147 | + "Send to CLIENT a MESSAGE :type string" |
123 | 148 | (let ((stream (client-stream client))) |
124 | 149 | (write-line message stream) |
125 | 150 | (finish-output stream))) |
126 | 151 |
|
127 | 152 | (defun client-reader-routine (client) |
| 153 | + "This function create a IO-bound procedure to act |
| 154 | + by reading the events of a specific CLIENT. |
| 155 | + On this software each client talks on your own thread." |
128 | 156 | (loop for message = (read-line (client-stream client)) |
129 | 157 | while (not (equal message "/quit")) |
130 | | - if (member message +commands-names+ :test #'equal) |
| 158 | + if (member message *commands-names* :test #'equal) |
131 | 159 | do (send-message client (call-command-by-name message)) |
132 | 160 | else |
133 | 161 | when (> (length message) 0) |
|
136 | 164 | finally (client-delete client))) |
137 | 165 |
|
138 | 166 | (defun client-reader (client) |
| 167 | + "This procedure is a wrapper for CLIENT-READER-ROUTINE |
| 168 | + treating all the possible errors based on HANDLER-CASE macro." |
139 | 169 | (handler-case (client-reader-routine client) |
140 | 170 | (end-of-file () (client-delete client)) |
141 | 171 | (sb-int:simple-stream-error () |
|
150 | 180 | (client-delete client))))) |
151 | 181 |
|
152 | 182 | (defun create-client (connection) |
| 183 | + "This procedure create a new client based on CONNECTION made by |
| 184 | + USOCKET:SOCKET-ACCEPT. This shit create a lot of side effects as messages |
| 185 | + if the debug is on because this makes all the log stuff to make analysis" |
153 | 186 | (debug-format t "Incoming connection from ~a ~%" (socket-peer-address connection)) |
154 | 187 | (let ((client-stream (socket-stream connection))) |
155 | 188 | (write-line "> Type your username: " client-stream) |
156 | 189 | (finish-output client-stream) |
157 | 190 | (let ((client (make-client :name (read-line client-stream) |
158 | 191 | :socket connection |
159 | 192 | :address (socket-peer-address connection)))) |
160 | | - (sb-thread:with-mutex (*client-mutex*) |
| 193 | + (with-mutex (*client-mutex*) |
161 | 194 | (debug-format t "Added new user ~a@~a ~%" |
162 | 195 | (client-name client) |
163 | 196 | (client-address client)) |
164 | 197 | (push client *clients*)) |
165 | 198 | (push-message "@server" (format nil "The user ~s joined to the party!" (client-name client))) |
166 | | - (sb-thread:make-thread #'client-reader |
| 199 | + (make-thread #'client-reader |
167 | 200 | :name (format nil "~a reader thread" (client-name client)) |
168 | 201 | :arguments (list client))))) |
169 | 202 |
|
170 | 203 | ;; a function defined to handle the errors of client thread |
171 | 204 | (defun safe-client-thread (connection) |
| 205 | + "This function is a wrapper for CREATE-CLIENT treating the |
| 206 | +exceptions." |
172 | 207 | (handler-case (create-client connection) |
173 | 208 | (end-of-file () nil) |
174 | 209 | (usocket:address-in-use-error () nil))) |
175 | 210 |
|
176 | 211 | (defun message-broadcast () |
177 | | - (loop when (sb-thread:wait-on-semaphore *message-semaphore*) |
| 212 | + "This procedure is a general independent thread to run brodcasting |
| 213 | + all the clients when a message is ping on this server" |
| 214 | + (loop when (wait-on-semaphore *message-semaphore*) |
178 | 215 | do (let ((message (formated-message (pop *messages-stack*)))) |
179 | 216 | (push message *messages-log*) |
180 | 217 | (loop for client in *clients* |
|
183 | 220 | (sb-bsd-sockets:not-connected-error () (client-delete client))))))) |
184 | 221 |
|
185 | 222 | (defun connection-handler (socket-server) |
| 223 | + "This is a special thread just for accepting connections from SOCKET-SERVER |
| 224 | + and creating new clients from it." |
186 | 225 | (loop for connection = (socket-accept socket-server) |
187 | | - do (sb-thread:make-thread #'safe-client-thread |
188 | | - :arguments (list connection) |
189 | | - :name "create client"))) |
| 226 | + do (make-thread #'safe-client-thread |
| 227 | + :arguments (list connection) |
| 228 | + :name "create client"))) |
190 | 229 |
|
191 | 230 | (defun server-loop (socket-server) |
| 231 | + "This is the general server-loop procedure. Create the threads |
| 232 | + necessary for the basic working state of this chat. The main idea |
| 233 | + is creating a MESSAGE-BROADCAST procedure and CONNECTION-HANDLER |
| 234 | + procedure running as separated threads. |
| 235 | +
|
| 236 | + The first procedure send always a new message too all clients |
| 237 | + defined on *clients* when *messages-semaphore* is signalized. |
| 238 | + The second procedure is a general connection-handler for new |
| 239 | + clients trying connecting to the server." |
192 | 240 | (format t "Running server... ~%") |
193 | | - (let* ((connection-thread (sb-thread:make-thread #'connection-handler |
| 241 | + (let* ((connection-thread (make-thread #'connection-handler |
194 | 242 | :arguments (list socket-server) |
195 | 243 | :name "Connection handler")) |
196 | | - (broadcast-thread (sb-thread:make-thread #'message-broadcast |
| 244 | + (broadcast-thread (make-thread #'message-broadcast |
197 | 245 | :name "Message broadcast"))) |
198 | | - (sb-thread:join-thread connection-thread) |
199 | | - (sb-thread:join-thread broadcast-thread))) |
| 246 | + (join-thread connection-thread) |
| 247 | + (join-thread broadcast-thread))) |
200 | 248 |
|
201 | 249 | (defun main () |
| 250 | + "Well, this function run all the necessary shits." |
202 | 251 | (let ((socket-server (socket-listen *host* *port*))) |
203 | 252 | (unwind-protect (handler-case (server-loop socket-server) |
204 | 253 | (usocket:address-in-use-error () |
|
0 commit comments