
1.0 Introduction
Most chat programs are text based, and do not support multi languages. In this article, I would like to share with the reader, the techniques I used to implement multilingual support and picture/media transfer in a chat program.
2.0 Streams and Protocols
A stream is a continuous flow of bytes. For a chat server and a chat client to communicate over the network, each would read and write to the network stream. The network stream is duplex, and bytes transmitted and read are always in FIFO order.
When a chat client is connected to a chat server, they would have established a common network stream to use.
However, for any meaningful communication, there must be some rules and order. These rules and order would be known as the communication protocols. There are at least a few layers of protocols.
At the lowest level, the communicating parties would need to know how to break a continuous flow of bytes into packets/frames. These rules would be referred to as the Network Protocol.
At the next level, the parties would need to interpret the packets/frames. These rules would be known as the Application Protocol(s).
3.0 Network Protocol
I would like to make a distinction between a text stream and a binary stream. In a text stream, only text characters are allowed. In a binary stream, all byte values (0x00 - 0xFF) are allowed.
One way to set markers in a text stream is to use a non-text byte as a marker. For example, the traditional C string is terminated by 0x00, which serves as an end marker.
For a binary stream, there is no way to set a marker because all byte values are legal. Thus, one way to break a binary stream to packets is for the parties to communicate to one another about the size of the binary bytes to follow, before actually sending the bytes.
In ChatStream.cs, the ChatStream class is implemented with methods for reading and writing text data (Read() and Write()), and also methods for reading and writing binary data (ReadBinary() and WriteBinary()).
Note that in the Write() method, we set a 0x03 as the marker, and Read() will read until the marker is encountered. For the WriteBinary() method, no marker is set, and ReadBinary() requires an input parameter to indicate the number of bytes to read.
public class ChatStream:IChatStream
{
public string Read(NetworkStream n)
{
byte[] bytes=new byte[512];
int totalbytes=0;
while(true){
int i=n.Read(bytes,totalbytes,2);
if(i==2)
{
if(bytes[totalbytes]==(byte)0x03)
if(bytes[totalbytes+1]==(byte)0x00)
break;
}
else
{
return "";
}
totalbytes +=i;
}
UnicodeEncoding Unicode = new UnicodeEncoding();
int charCount = Unicode.GetCharCount(bytes, 0, totalbytes);
char[] chars = new Char[charCount];
Unicode.GetChars(bytes, 0, totalbytes, chars, 0);
string s=new string(chars);
return s;
}
public void Write(NetworkStream n,string s)
{
s=s+new string((char)0x03,1);
UnicodeEncoding Unicode = new UnicodeEncoding();
byte[] bytes=Unicode.GetBytes(s);
n.Write(bytes,0,bytes.Length );
n.Flush();
}
public byte[] ReadBinary(NetworkStream n,int numbytes)
{
int totalbytes=0;
byte[] readbytes=new byte[numbytes];
while(totalbytes<numbytes)
{
int i=n.Read(readbytes,totalbytes,numbytes-totalbytes);
if(i==0)
return null;
totalbytes +=i;
}
return readbytes;
}
public void WriteBinary(NetworkStream n,byte[] b)
{
n.Write(b,0,b.Length);
n.Flush();
}
}
4.0 Unicode
The traditional C string is a single-byte character string. It is adequate for representing all ASCII characters. However, for languages where the character set has more than 256 characters, a single-byte character representation is no longer adequate.
In .NET as in VB, strings are all internally double-byte.
To manipulate the double-byte characters in the String class, we can make use of the UnicodeEncoding class.
This code fragment shows how to extract out all the double-byte bytes from a string:
UnicodeEncoding Unicode = new UnicodeEncoding();
byte[] bytes=Unicode.GetBytes(s);
Similarly, to construct a Unicode string from a byte array:
UnicodeEncoding Unicode = new UnicodeEncoding();
int charCount = Unicode.GetCharCount(bytes, 0, totalbytes);
char[] chars = new Char[charCount];
Unicode.GetChars(bytes, 0, totalbytes, chars, 0);
string s=new string(chars);
Although the TextBox and RichTextBox controls in .NET support Unicode, a Unicode supported font is needed for display of Unicode characters, and an appropriate IME (Input Method Editor) is needed for Unicode input.
For this program, I make use of Arial Unicode MS which comes with Windows XP.
5.0 Sending and Receiving Pictures
If you ever use a binary editor to view a picture file (JPG or BMP), you will know that all byte values are possible in a picture file. To transfer picture binary data from a file or memory stream, we would not be able to set a marker to break the stream as what we can do for text data.
For this program:
The protocol for sending a picture is as follows:
- The client sends a command: send pic:<target>.
- When the server receives the command, it will check if <target> has an active connection. It then replies with “<server> send pic”.
- When the client receives this special message, it will send a text message to indicate to the server the number of bytes of binary data that will be sent over. Following that, the binary data are then sent.
- The server uses the
ChatStream ReadBinay method to get the binary data, and then saves the data to a file marked with the sender and target names.
- The server will then send a message to the <target> that there is a picture ready for it to retrieve.
The protocol for getting a picture is as follows:
- The client sends a command: get pic:<sender>.
- When the server receives the command, it will first check if there is a file with the <sender> and the client name. If so, it will send the reply “<server> get pic”. It will then send a text message to indicate to the client the number of bytes to read. Then the binary data will be sent over.
- The client uses the common
ChatStream ReadBinary method to get the binary data and display the image in the RichTextBox.
The server code for acting on the send_pic and get_pic commands from the client:
private void action_send_pic()
{
string[] s=readdata.Split(':');
string name="";
if (s.Length==3)name=s[2];
TcpClient t=null;
if (chatserver.FindUserRoom(name)!=0)
t=(TcpClient)chatserver.ClientConnections[name.ToUpper()];
if((t!=null))
{
chatserver.Write(client.GetStream(),
ChatProtocolValues.SEND_PIC_MSG);
string snumbytes=chatserver.Read(client.GetStream());
int numbytes=int.Parse(snumbytes);
byte[] b=chatserver.ReadBinary(client.GetStream(),numbytes);
if (b==null)
{
chatserver.Write(client.GetStream(),
"server> Transmission Error");
return;
}
FileStream f=new FileStream(nickname+"_"+name+".jpg",FileMode.Create);
f.Write(b,0,b.Length);
f.Close();
chatserver.Write (t.GetStream(),
ChatProtocolValues.PIC_FROM_MSG(nickname,name));
chatserver.Write(client.GetStream(),
ChatProtocolValues.PIC_SEND_MSG(nickname));
}
else
{
chatserver.Write(client.GetStream(),
ChatProtocolValues.USER_NOT_FOUND_MSG(name));
}
}
private void action_get_pic()
{
string[] s=readdata.Split(':');
string sender="";
string picname="";
if(s.Length==3)sender=s[2];
picname=sender + "_" + nickname + ".jpg";
if(!File.Exists(picname))
chatserver.Write(client.GetStream(),
ChatProtocolValues.PIC_NOT_FOUND_MSG(picname));
else
{
FileStream f=new FileStream(picname,FileMode.Open);
FileInfo fi=new FileInfo(picname);
byte[] b=new byte[fi.Length];
f.Read(b,0,b.Length);
f.Close();
chatserver.Write (client.GetStream(),
ChatProtocolValues.GET_PIC_MSG);
chatserver.Write(client.GetStream(),""+b.Length);
chatserver.WriteBinary(client.GetStream(),b);
chatserver.Write(client.GetStream(),
ChatProtocolValues.PIC_SEND_ACK_MSG);
TcpClient t=null;
if (chatserver.FindUserRoom(sender)!=0)
t=(TcpClient)chatserver.ClientConnections[sender.ToUpper()];
if(t!=null)
chatserver.Write(t.GetStream(),
ChatProtocolValues.GOTTEN_PIC_MSG(nickname));
}
}
The client responses to the server's messages:
private void action_server_send_pic()
{
MemoryStream ms=new MemoryStream();
pic.Image.Save(ms,ImageFormat.Jpeg);
byte[] buf=ms.GetBuffer();
ms.Close();
Write("" + buf.Length);
WriteBinary(buf);
Console.WriteLine("Send: {0} bytes", buf.Length);
}
private void action_server_get_pic()
{
string snumbytes=Read();
int numbytes=int.Parse(snumbytes);
byte[] readbytes=ReadBinary(numbytes);
if(readbytes==null)
{
Console.WriteLine("Error getting picture");
responseData="server> Error getting picture";
action_message();
return;
}
MemoryStream ms=new MemoryStream(readbytes);
Image img=Image.FromStream(ms);
ms.Close();
PastePictureToRTB(img);
}
6.0 Sending, Receiving, and Playing Media Clips
The protocol for transferring media clips is very similar to that for picture transfer. The main difference is that unlike a picture which is basically copied from a picture box in the UI and saved to a file with a fixed jpg extension, the media clips are just files that are tagged and stored by the program, and can have various different extensions. The extension for these files has to be maintained as the media player relies on the extension to play the files.
When sending the binary data of a media clip to the server, the extension of the clip must be conveyed. And when the receiver retrieves the binary data from the server, the extension must also be made known so that the media clip file can be recreated with the correct extension.
To resolve this problem, there is a slight change in the protocol. When sending media clip data to the server, the sender first sends a three-character extension, followed by the number of bytes of binary data, and then finally, the binary data. The server first reads the extension, saves the extension to a file named <sender>_<target> (without the extension), and then saves the binary data to a file named <sender>_<target>.<ext>.
private void action_server_send_media()
{
if(shp1.Text.Equals("Empty")){
Write(""+0);
return;
}
String ext=shp1.Text.Substring(shp1.Text.Length-3);
Write(ext);
FileInfo fi=new FileInfo(_currentpath +"\\"+nickname+"."+ext);
FileStream f=new FileStream(_currentpath +"\\"+nickname+"."+ext,
FileMode.Open);
byte[] b=new byte[fi.Length];
f.Read(b,0,b.Length);
f.Close();
Write("" + b.Length);
//Thread.Sleep(500);
WriteBinary(b);
//Console.WriteLine("Send: {0} bytes", b.Length);
}
Similarly, when the receiver retrieves the media clip, the server first locates the file that stores the extension, retrieves the extension, and the retrieves the file <sender>_<target>.<ext>, and then sends the extension followed by the media clip binary data to the receiver.
private void action_server_get_media()
{
string ext=Read();
string snumbytes=Read();
int numbytes=int.Parse(snumbytes);
byte[] readbytes=ReadBinary(numbytes);
if(readbytes==null)
{
responseData="server> Error getting picture";
action_message();
return;
}
FileStream f=new FileStream(_currentpath +
"\\"+nickname+"_received."+ext,
FileMode.Create);
f.Write(readbytes,0,numbytes);
f.Close();
// shpR.Text=""+(numbytes/1000)+"KB";
rtb.SelectionStart=rtb.Text.Length;
_media_id++;
string c_currentpath=_currentpath.Replace(" ","@");
rtb.SelectedText="\nfile: "\\"+nickname+"_received" +
_media_id+"."+ext;
File.Copy(_currentpath +"\\"+nickname+"_received."+ext,
_currentpath +"\\"+nickname+"_received" +
_media_id+"."+ext,true);
}
To play the media clip, the system must have the media player installed. The chat client locates the media player from the Windows registry and sends the clip to be played by the media player.
public class WinMediaPlayer
{
public static string GetMediaPlayerDirectory()
{
try
{
Microsoft.Win32.RegistryKey localmachineregkey=
Microsoft.Win32.Registry.LocalMachine;
Microsoft.Win32.RegistryKey mediaplayerkey=
localmachineregkey.OpenSubKey(@"SOFTWARE\Microsoft\MediaPlayer");
return (string)mediaplayerkey.GetValue("Installation Directory");
}
catch
{
return "";
}
}
public static void Play(IntPtr hwnd,string strFileName)
{
if(!ChatClient.MediaPlayerDirectory.Equals(""))
Helpers.ShellExecute(hwnd,"open","wmplayer",
"\""+strFileName+"\"",ChatClient.MediaPlayerDirectory ,
Helpers.SW_NORMAL);
}
}
7.0 Server Program
The server program starts by creating the requested number of chat rooms, and reads out the users by deserializing the users.bin file. Then, it continues to listen for connections. Once a connection is established, it sprouts a new SocketHelper object to manage the communication with the connected client.
public class ChatServer:ChatStream,IChatServer
{
public ChatServer(int port_no,int num_room)
{
this.port_no =port_no;
this.num_room=num_room;
num_per_room=DEFAULT_NUM_PER_ROOM;
listener=new TcpListener(IPAddress.Any,this.port_no);
DeserializeChatUsers("users.bin");
roomusers=new Hashtable[num_room];
for(int i=0;i<num_room;i++)
roomusers[i]=new Hashtable();
connections=new Hashtable();
Listener.Start();
while(true)
{
Console.WriteLine("Waiting for connection...");
TcpClient client=Listener.AcceptTcpClient();
SocketHelper sh=new SocketHelper(this,client);
Console.WriteLine("Connected");
}
}
}
To run the server:
chatserver <port_number> <num_room>
E.g.: chatserver 1300 10 uses port 1300, and creates 10 chat rooms.
8.0 Client Program
The client program starts by connecting to the server. It then attempts to perform authentication. If successful, a thread is started to listen for the server's messages. A form is then loaded to show the UI and handle user's interaction.
public ChatClient(string _host,int _port)
{
_currentpath=Path.GetDirectoryName(
Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName);
host =_host;
port=_port;
try
{
tcpc=Connect(host,port);
stream=tcpc.GetStream();
Stream=stream;
}
catch {
Environment.Exit(0);
}
init_components();
tl=new Thread(new ThreadStart(Listen));
tl.Start();
}
To run the client program:
chatclient <server_name> <port_no>
E.g.: chatserver localhost 1300.
9.0 Chat Client User Interface
The user can either key in commands at the textbox, or use the menu system. The code below shows the implementation of the menu system:
private void init_components()
{
cfont=new Font("Arial",14);
this.Text="Client ISS Chat";
this.ClientSize = new System.Drawing.Size(400,570);
this.WindowState=FormWindowState.Minimized;
this.TopMost=true;
shp1=new ShapeControl.ShapeControl();
this.Controls.Add(shp1);
shp1.BackColor = System.Drawing.Color.FromArgb(((System.Byte)(124)),
((System.Byte)(92)), ((System.Byte)(159)),
((System.Byte)(83)));
shp1.BorderColor = System.Drawing.Color.FromArgb(((System.Byte)(177)),
((System.Byte)(131)), ((System.Byte)(255)),
((System.Byte)(4)));
shp1.BorderStyle = System.Drawing.Drawing2D.DashStyle.Solid;
shp1.BorderWidth = 3;
shp1.CenterColor = System.Drawing.Color.FromArgb(((System.Byte)(255)),
((System.Byte)(0)), ((System.Byte)(0)));
shp1.Font = new System.Drawing.Font("Arial", 8F,
System.Drawing.FontStyle.Bold);
shp1.Location = new Point(5,495);
shp1.Shape = ShapeControl.ShapeType.RoundedRectangle;
shp1.Size = new System.Drawing.Size(150, 35);
shp1.SurroundColor = System.Drawing.Color.FromArgb(((System.Byte)(0)),
((System.Byte)(0)), ((System.Byte)(255)));
shp1.Text = "Empty";
shp1.UseGradient = false;
contextmenu_shp1=new ContextMenu();
contextmenu_shp1.MenuItems.Add(new
MenuItem("Load Media File",new EventHandler(LoadMedia)));
contextmenu_shp1.MenuItems.Add(new MenuItem("Play Media File",
new EventHandler(PlayMediaFile)));
shp1.ContextMenu=contextmenu_shp1;
rtb=new RichTextBox();
this.Controls.Add(rtb);
rtb.DetectUrls=true;
rtb.LinkClicked+=new LinkClickedEventHandler(rtb_LinkClicked);
rtb.Top=0;
rtb.Left=0;
rtb.Width=400;
rtb.Height=400;
rtb.BackColor=Color.LightBlue;
rtb.Font=new Font("Arial Unicode MS",10,FontStyle.Bold);
rtb.ReadOnly=true;
rtb.TabStop=false;
checkbox=new CheckBox();
this.Controls.Add(checkbox);
checkbox.Text="Auto Retrieve Picture/Media";
checkbox.Size=new Size(200,15);
checkbox.Checked=true;
checkbox.Location=new Point(5,405);
checkbox.TabStop=false;
button=new Button();
this.Controls.Add(button);
button.Text="Clear Messages";
button.Size= new Size(120,20);
button.Location=new Point(275,400);
button.TabStop=false;
contextmenu=new ContextMenu();
MenuItem menuItem1 =
new MenuItem("Clear Picture Box",
new EventHandler(ClearPicture));
MenuItem menuItem2 =
new MenuItem("Load Picture(s)",
new EventHandler(LoadPictureFile));
contextmenu.MenuItems.Add(menuItem1);
contextmenu.MenuItems.Add(menuItem2);
pic=new PictureBox();
this.Controls.Add(pic);
pic.BackColor = Color.White;
pic.Location = new Point(5, 425);
pic.Size = new Size(390, 60);
pic.BorderStyle=BorderStyle.FixedSingle;
pic.Image=new Bitmap(390,60,PixelFormat.Format32bppArgb);
pic.ContextMenu=contextmenu;
pic.TabStop=false;
textbox=new TextBox();
this.Controls.Add(textbox);
textbox.Location=new System.Drawing.Point(5, 540);
textbox.Size=new Size(390,18);
textbox.MaxLength=240;
textbox.TabIndex=0;
textbox.TabStop=true;
textbox.Font=new Font("Arial Unicode MS",12);
MainMenu mainMenu1 = new MainMenu();
MenuItem mItem1 = new MenuItem("&Command");
mItem1.Popup +=new EventHandler(CommandPopUp);
p_smItem5=new MenuItem(":send pic:");
p_smItem6=new MenuItem(":get pic:");
p_smItem7=new MenuItem(":private:");
p_smItemS1=new MenuItem(":send media:");
p_smItemS2=new MenuItem(":get media:");
mItem1.MenuItems.Add(new MenuItem(":help",
new EventHandler(Commands)));
mItem1.MenuItems.Add(new MenuItem(":list all",
new EventHandler(Commands)));
mItem1.MenuItems.Add(new MenuItem(":change room",
new EventHandler(Commands)));
mItem1.MenuItems.Add(new MenuItem(":which room",
new EventHandler(Commands)));
mItem1.MenuItems.Add(p_smItemS1);
mItem1.MenuItems.Add(p_smItemS2);
mItem1.MenuItems.Add(p_smItem5);
mItem1.MenuItems.Add(p_smItem6);
mItem1.MenuItems.Add(p_smItem7);
mItem1.MenuItems.Add(new MenuItem(":quit",
new EventHandler(Commands)));
MenuItem mItem2 = new MenuItem("&Picture");
mItem2.MenuItems.Add(new MenuItem("Clear Picture",
new EventHandler(ClearPicture)));
mItem2.MenuItems.Add(new MenuItem("Load Picture(s)",
new EventHandler(LoadPictureFile)));
MenuItem mItem3=new MenuItem("&Media");
mItem3.MenuItems.Add(new MenuItem("Load Media File",
new EventHandler(LoadMedia)));
mItem3.MenuItems.Add(new MenuItem("Play Media File",
new EventHandler(PlayMediaFile)));
mainMenu1.MenuItems.Add(mItem1);
mainMenu1.MenuItems.Add(mItem2);
mainMenu1.MenuItems.Add(mItem3);
this.Menu = mainMenu1;
this.SizeChanged +=new EventHandler(textbox_SizeChanged);
textbox.KeyPress += new KeyPressEventHandler(textbox_KeyPressed);
pic.MouseMove += new MouseEventHandler(this.pic_MouseMove);
pic.MouseDown += new MouseEventHandler(this.pic_MouseDown);
this.Activated +=new EventHandler(form_Activated);
this.Closing +=new CancelEventHandler(form_Closing);
rtb.TextChanged +=new EventHandler(rtb_TextChanged);
button.Click +=new EventHandler(button_Click);
memberColor=new Hashtable();
}
private void CommandPopUp(object sender,System.EventArgs e)
{
pause_listening=true;
Thread.Sleep(0);
responseData="";
Write(":list all");
if(responseData==""){
responseData=Read();
}
string[] s=responseData.Split('\n');
ArrayList arrlist=new ArrayList();
foreach(string s1 in s)
if(s1.Trim()!="")
{
string[] s2=s1.Trim().Split(':');
if ((s2.Length==2)&& (s2[0].IndexOf("server>")<0))
arrlist.Add(s2[0].Trim());
}
p_smItemS1.MenuItems.Clear();
p_smItemS2.MenuItems.Clear();
p_smItem5.MenuItems.Clear();
p_smItem6.MenuItems.Clear();
p_smItem7.MenuItems.Clear();
foreach(string name in arrlist)
{
p_smItemS2.MenuItems.Add(new MenuItem(name,
new EventHandler(Param1Command)));
if(!shp1.Text.Equals("Empty"))
p_smItemS1.MenuItems.Add(new MenuItem(name,
new EventHandler(Param1Command)));
p_smItem5.MenuItems.Add(new MenuItem(name,
new EventHandler(Param1Command)));
p_smItem6.MenuItems.Add(new MenuItem(name,
new EventHandler(Param1Command)));
p_smItem7.MenuItems.Add(new MenuItem(name+":",
new EventHandler(Param1Command)));
}
responseData="";
pause_listening=false;
Thread.Sleep(0);
}
private void Param1Command(object sender,System.EventArgs e)
{
string p=((MenuItem)((MenuItem)sender).Parent).Text.Trim();
string s=((MenuItem)sender).Text.Trim();
if (p.ToUpper()==":PRIVATE:")
{
textbox.Text=p+s+"<Key in Message>";
textbox.SelectionStart=textbox.Text.Length-16;
textbox.SelectionLength=16;
}
else
Write(p+s);
}
10.0 Conclusion
I hope that the readers will benefit from this article and its associated code. I would welcome any comments and contributions.
Happy picture chatting.
History
- Version 1.0: Sep 2004.
- Version 2.0: Jun 2006.
- Version 3.0: Jul 2008.
- I have made some amendments — source now compiles succesfully using VC# 2008 Express Edition