Using Sockets

Contributed by dohcan
This tutorial attempts to bring you up to speed with the basics of using sockets. It is assumed that you have good scripting knowledge. This tutorial uses a small "dcc chat" simulator called SockChat. It functions the same as /dcc chat. This is NOT a tutorial on DCC Chats.

NOTE: At the bottom of this tutorial, the entire source code is provided.
Sockets in mIRC
You can use sockets in mIRC to connect to other machines, allow other machines to connect to you, or both. Using sockets, you can create a myriad of applications within mIRC such as an FTP client, a partyline, or even another irc client.

In this tutorial, we are going to simulate a DCC Chat with sockets and custom @windows. Before you can understand how to do this, you must first know how DCC Chat works. Luckily, it's quite simple. After the new socket is created, you listen for data and act accordingly. You also send data to the other party.
Before We Start
In this example, I have created a few miscellaneous aliases to make controlling the sockets easier. These are all local aliases and are only used internally by the script.

sc.close <socket>
This alias verifies that the socket still exists, and if it does, it closes it.
alias -l sc.close if ($sock($1)) sockclose $1
sc.echo <nick> <color> <text>
This alias simply echoes text in the specified color to the correct window.
alias -l sc.echo echo $2 -i2 @SockChat: $+ $1 $3-
I also make many references to $gettok() in this example. I use $gettok() to get the nickname from the socket name or the @window name. You don't have to name your sockets like this, you can use the .mark property of a socket to store information about the socket. I simply used this method to allow multiple dcc chats at one time.
Setting Up
The first thing that needs to be done is setting up the listen server. The way to do this is to create a unique socket and attach a port to it. The following code below illustrates how:
; main alias
; usage: /sockchat <nickname>
alias sockchat {
  ; make sure your parameters are correct
  if ($1 == $null) {
    echo -a * /sockchat: You must specify a nickname
    return
  }

  ; declare variables here
  ; %sc.port is the port we are listening for the other party on
  ; %sc.sock is the name of the listening socket - sc.listen.<nickname>
  var %sc.port = 1024
  var %sc.sock = sc.listen. $+ $1

  ; iterate through until you find the first open port >= 1024
  while ($portfree(%sc.port) == $false) {
    inc %sc.port
  }

  ; listen for the dcc chat
  socklisten sc.listen. $+ $1 %sc.port

  ; notice the client that you are initiating a dcc chat
  .notice $1 DCC Chat ( $+ $ip $+ )

  ; do the actual dcc chat request
  ; DCC CHAT chat <ip address - long> <port>
  .raw privmsg $1 : $+ $chr(1) $+ DCC CHAT chat $longip($ip) %sc.port $+ $chr(1)

  ; set a 60 second timer to close the socket (timeout)
  .timer 1 60 sc.close %sc.sock

  ; open the window and name it @SockChat:<nickname>
  ; notice that the @windows are named SockChat:<nickname> - this is to keep
  ; multiple chats unique.
  window -kae @SockChat: $+ $1

  ; echo miscellaneous info
  sc.echo $1 $colour(info2) DCC Chat with $1
  sc.echo $1 $colour(info) Waiting for other party to accept...
}
As you can see, this alias sets up the listen server and configures a custom @window to handle the new chat. The most important part about this procedure is finding an open port to listen on. This is to avoid conflicts with other programs that access sockets.
Accepting the Connection
Use the on socklisten event to wait for the other party to connect to you. Once we get the connection, we accept it and close the old listening socket.
on *:socklisten:sc.listen.*: {
  ; get the nickname from the sockname
  var %sc.nick = $gettok($sockname, 3, 46)
  ; accept the connection to a new socket
  ; name it "sc.accept.<nick>" so we can identify it
  sockaccept sc.accept. $+ %sc.nick
  ; close the old listening socket
  sockclose $sockname
  ; notify that a connection has been made
  sc.echo %sc.nick $colour(info) Connection established!
}
Again, depending on the type of application you are creating, closing the listening socket is not always needed.
Handling the Data
After the connection has been made, you need to set up two handlers for manipulating the data. You need one to receive the data from the other party and you need one to send your data. To do this, you would use the on sockread to get the data and on input to send the data.

Here is the on sockread code:
on *:sockread:sc.accept.*: {
  ; get the nickname from the sockname
  var %sc.tmp, %sc.nick = $gettok($sockname, 3, 46)
  ; read the text received into a temporary variable
  sockread %sc.tmp
  ; if anything was read, echo the text
  if ($sockbr) sc.echo %sc.nick $colour(normal) < $+ %sc.nick $+ > %sc.tmp
}
Here is the on input code:
on *:input:@SockChat*: {
  ; make sure the user didn't enter a /command
  ; or if he did, make sure he used CTRL+enter
  if (($left($1, 1) != /) || ($ctrlenter)) {
    ; get the nickname from the window
    var %sc.nick = $gettok($active, 2, 58)
    ; the corresponding socket = sc.accept.<nickname>
    var %sc.sock = sc.accept. $+ %sc.nick

    ; make sure the socket still exists
    if ($sock(%sc.sock)) {
      ; send the text you typed with a CRLF appended to the end (-n)
      sockwrite -n %sc.sock $1-
      ; echo what you said
      sc.echo %sc.nick $colour(normal) < $+ $me $+ $+ > $1-
    }
    ; halt the default action
    halt
  }
}
Closing the Connection
In order to properly handle closing the connection (or having the connection closed), we need two more events - on close and on sockclose. We use on close to handle closing the socket ourself (via closing the @window) and we use on sockclose to handle a termination of the socket by someone or something else.

Here is the on close code, in which we use our /sc.close alias to verify that the connection is still alive. If it isn't, then we do not have to do anything. This is called whenever *you* close the window.
on *:close:@SockChat*: sc.close sc.accept. $+ $gettok($target, 2, 58)
Here is the code for on sockclose. This is called whenever the other party closes the chat or if the connection is terminated for any reason other than your doing.
on *:sockclose:sc.accept.*: {
  ; get the nickname from the sockname
  var %sc.nick = $gettok($sockname, 3, 46)
  ; echo that the connection has been lost
  sc.echo %sc.nick $colour(info) DCC Chat connection with %sc.nick lost...
}
Extra Notes
In this example, there is only minimal error checking, but one thing that is important is when reading data (on sockread), always check that the identifier $sockbr (bytes read) is more than zero. You can also check $sockerr to see if an error has occurred in the read event.

Below is the full source code without the comments to save space. Enjoy!
alias sockchat {
  if ($1 == $null) {
    echo -a * /sockchat: You must specify a nickname
    return
  }
  var %sc.port = 1024
  var %sc.sock = sc.listen. $+ $1
  while ($portfree(%sc.port) == $false) {
    inc %sc.port
  }
  socklisten sc.listen. $+ $1 %sc.port
  .notice $1 DCC Chat ( $+ $ip $+ )
  .raw privmsg $1 : $+ $chr(1) $+ DCC CHAT chat $longip($ip) %sc.port $+ $chr(1)
  .timer 1 60 sc.close %sc.sock
  window -kae @SockChat: $+ $1
  sc.echo $1 $colour(info2) DCC Chat with $1
  sc.echo $1 $colour(info) Waiting for other party to accept...
}

alias -l sc.close if ($sock($1)) sockclose $1
alias -l sc.echo echo $2 -i2 @SockChat: $+ $1 $3-

on *:socklisten:sc.listen.*: {
  var %sc.nick = $gettok($sockname, 3, 46)
  sockaccept sc.accept. $+ %sc.nick
  sockclose $sockname
  sc.echo %sc.nick $colour(info) Connection established!
}

on *:sockclose:sc.accept.*: {
  var %sc.nick = $gettok($sockname, 3, 46)
  sc.echo %sc.nick $colour(info) DCC Chat connection with %sc.nick lost...
}

on *:sockread:sc.accept.*: {
  var %sc.tmp, %sc.nick = $gettok($sockname, 3, 46)
  sockread %sc.tmp
  if ($sockbr) sc.echo %sc.nick $colour(normal) < $+ %sc.nick $+ > %sc.tmp
}

on *:input:@SockChat*: {
  if (($left($1, 1) != /) || ($ctrlenter)) {
    var %sc.nick = $gettok($active, 2, 58)
    var %sc.sock = sc.accept. $+ %sc.nick
    if ($sock(%sc.sock)) {
      sockwrite -n %sc.sock $1-
      sc.echo %sc.nick $colour(normal) < $+ $me $+ $+ > $1-
    }
    halt
  }
}

on *:close:@SockChat*: sc.close sc.accept. $+ $gettok($target, 2, 58)
All content is copyright by mircscripts.org and cannot be used without permission. For more details, click here.