Tiếp tục trên Solution hiện tại tạo thêm 2 project có dạng Class Library đặt tên là Command và Server.. Bước 2: vào project Command tạo 2 file có tên là Enum.cs và Command.cs.. Bước 3: m
Trang 1Đại Học Quốc Gia Thành Phố Hồ Chí Minh
Trường Đại học Công nghệ Thông tin
Lớp K2C6 Hướng dẫn chi tiết làm 1 chương trình Chat đơn giản
Giáo viên hướng dẫn: Phạm Thi Vương
I Giới thiệu chung về cấu trúc.
- Chat theo mô hình Client-Server, sử dụng giao thức TCP.
- Đơn vị truyền thông tin là Command.
- Sử dụng các namespace chính là:
o System.Net;
o System.Net.Sockets;
o System.CompomentModel;
II Hướng dẫn chi tiết:
1 Lập trình Server:
Bước 1: vào Visual C# tạo 1 project mới có tên là UITChatServer, có dạng là
Windows Form Đặt tên cho class chính là frmServer Tiếp tục trên Solution hiện tại tạo thêm 2 project có dạng Class Library đặt tên là Command và Server.
Bước 2: vào project Command tạo 2 file có tên là Enum.cs và Command.cs.
Bước 3: mở file Enum.cs nhập vào đoạn code sau đây để định nghĩa các loại
command sẽ dùng, khi muốn update thì cần chú ý nội dụng của file này.
{
Message,// Để gửi message
ClientList,//Để gửi danh sách client online
NameExists,// thông báo tên vừa đăng nhập đã có người sử dụng
Login, //thông báo đăng nhập
Logout,//thông báo đăng xuất
}
Bươc 4: Mở file Command.cs và nhập vào đoạn code sau dùng định nghĩa 1 đối
tượng command dùng để truyền trên mạng, gồm có CommandType, SenderName, SenderIP, TargerIP,
Trang 2{
private IPAddress senderIP;// địa chỉ IP máy gửi
public IPAddress SenderIP
{
get { return senderIP; }
set { senderIP = value; }
}
public string SenderName
{
get { return senderName; }
set { senderName = value; }
}
private CommandType cmdType;//loại Command được gửi
public CommandType CommandType
{
get { return cmdType; }
set { cmdType = value; }
}
private IPAddress target;// địa chỉ IP máy nhận
public IPAddress Target
{
get { return target; }
set { target = value; }
}
private string commandBody;//nội dung cần gửi
public string MetaData
{
get { return commandBody; }
set { commandBody = value; }
}
// 2 contructor
public Command(CommandType type, IPAddress targetMachine, string
metaData)
{
this.cmdType = type;
this.target = targetMachine;
this.commandBody = metaData;
}
public Command(CommandType type, IPAddress targetMachine)
{
this.cmdType = type;
this.target = targetMachine;
this.commandBody = "";
}
}
}
Bươc 5: Chuyển sang project Server Ta Reference đến project Command Tạo 3
file là ClientManager.cs , Server.cs và EventArgs.cs
Trang 3Bước 6: Mở file EventArgs.cs, nhập vào nội dung sau, định nghĩa các class thừa kế
từ class EventArg và các delegate để dùng khai báo các Event trong 2 file còn lại.
using System;
using System.Net;
using System.Net.Sockets;
using UITChat;//chú ý phải using đến class command
namespace Server
{
public delegate void UserOnlineHandler(object sender, UserEventArg user);
public delegate void UserLogoutHandler(object sender, UserEventArg user);
public delegate void CommandReceivedEventHandler(object sender,CommandEventArgs e); public delegate void DisconnectedEventHandler(object sender, ClientEventArgs e); public class UserEventArg : EventArgs
{
private string _userName;//tên người dùng
private IPAddress _ipaddress;//địa chỉ IP
public string UserName
{
get
{
return _userName;
}
set
{
_userName = value;
}
}
public UserEventArg(string Name)
{
_ipaddress = null;
_userName = Name;
}
public UserEventArg(IPAddress address, string Name)
{
_ipaddress = address;
_userName = Name;
}
}
public class CommandEventArgs : EventArgs
{
private Command command;// đối tượng Command
public Command Command
{
get { return command; }
}
public CommandEventArgs(Command cmd)
{
this.command = cmd;
}
}
public class ClientEventArgs : EventArgs
{
private Socket socket; //Socket của client
public IPAddress IP
{
get { return ((IPEndPoint)this.socket.RemoteEndPoint).Address; }
}
public int Port
{
Trang 4get { return ((IPEndPoint)this.socket.RemoteEndPoint).Port; }
}
public ClientEventArgs(Socket clientManagerSocket)
{
this.socket = clientManagerSocket;
}
}
}
Bước 7: Mở file ClientManager.cs, ta tạo 1 class có tên là ClientManager Trong
class này ta khai báo 1 số thuộc tính và contructor như sau:
{
get
{
try
{
return ((IPEndPoint)this.socket.RemoteEndPoint).Address;
}
catch
{
return IPAddress.None;
}
}
}
{
get
{
if (this.socket != null)
return ((IPEndPoint)this.socket.RemoteEndPoint).Port;
else
return -1;
}
}
{
set { this.clientName = value; }
}
{
this.socket = clientSocket;
this.networkStream = new NetworkStream(this.socket);
this.bwReceiver = new BackgroundWorker();
this.bwReceiver.DoWork += new DoWorkEventHandler(Receive);
this.bwReceiver.RunWorkerAsync();
Trang 5#endregion
Bước 8: Ta viết hàm Receive chạy trên Thread bwReceiver Chú ý ta dùng
Encoding để chuyển dữ liệu sang kiểu dữ liệu cũ trước khi Encode ở phía máy gửi Đây là hàm để chạy Event DoWorkEventHandler trong BackgroudWorker Một class kế thừa từ Thread, giúp làm việc trên Thread đơn giản hơn.
private void Receive(object sender, DoWorkEventArgs e)
{
while (this.socket.Connected)
{
try
{
//Đọc commandtype
byte[] buffer = new byte[4];
int readBytes = this.networkStream.Read(buffer, 0, 4);
if (readBytes == 0)
break;
CommandType cmdType = (CommandType)(BitConverter.ToInt32(buffer, 0)); //Đọc kích thước của SenderIP
buffer = new byte[4];
readBytes = this.networkStream.Read(buffer, 0, 4);
if (readBytes == 0)
break;
int senderIPSize = BitConverter.ToInt32(buffer, 0);
//Đọc senderIP
buffer = new byte[senderIPSize];
readBytes = this.networkStream.Read(buffer, 0, senderIPSize);
if (readBytes == 0)
break;
IPAddress senderIP =
IPAddress.Parse(System.Text.Encoding.ASCII.GetString(buffer));
//Đọc kích thước của SenderName
buffer = new byte[4];
readBytes = this.networkStream.Read(buffer, 0, 4);
if (readBytes == 0)
break;
int senderNameSize = BitConverter.ToInt32(buffer, 0);
//Đọc SenderName
buffer = new byte[senderNameSize];
readBytes = this.networkStream.Read(buffer, 0, senderNameSize);
if (readBytes == 0)
break;
string senderName = System.Text.Encoding.Unicode.GetString(buffer);
//Đọc kích thước của TargetIp
string cmdTarget = "";
buffer = new byte[4];
readBytes = this.networkStream.Read(buffer, 0, 4);
if (readBytes == 0)
break;
int ipSize = BitConverter.ToInt32(buffer, 0);
//Đọc TargerIp
buffer = new byte[ipSize];
readBytes = this.networkStream.Read(buffer, 0, ipSize);
Trang 6if (readBytes == 0)
break;
cmdTarget = System.Text.Encoding.ASCII.GetString(buffer);
//Đọc kích thước của Metadata
string cmdMetaData = "";
buffer = new byte[4];
readBytes = this.networkStream.Read(buffer, 0, 4);
if (readBytes == 0)
break;
int metaDataSize = BitConverter.ToInt32(buffer, 0);
//Đọc metadata
buffer = new byte[metaDataSize];
readBytes = this.networkStream.Read(buffer, 0, metaDataSize);
if (readBytes == 0)
break;
cmdMetaData = System.Text.Encoding.Unicode.GetString(buffer);
//đóng gói thành kiểu command
Command cmd = new Command(cmdType, IPAddress.Parse(cmdTarget), cmdMetaData); cmd.SenderIP = this.IP;
cmd.SenderName = this.ClientName;
//phát sự kiện đã nhận xong 1 command
this.OnCommandReceived(new CommandEventArgs(cmd));
}
catch
{
break;//gặp bất kỳ lỗi nào sẽ tự thoát
}
}
//phát sự kiện mất kết nối
this.OnDisconnected(new ClientEventArgs(this.socket));
//ngắt kết nối
this.Disconnect();
}
Bước 9: Ta viết hàm SendCommand để gửi 1 command đến Client mà nó quản lý
Quá trình gửi cũng được thực hiện trên 1 Thread riêng, là 1 đối tượng
BackgroundWorker Chú ý các hàm xử lý sự kiện cho đối tượng này được trình bày dưới hàm SendCommand.
{
if (this.socket != null && this.socket.Connected)
{
BackgroundWorker bwSender = new BackgroundWorker();
bwSender.DoWork += new DoWorkEventHandler(bwSender_DoWork);
bwSender.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(bwSender_RunWorkerCompleted);
bwSender.RunWorkerAsync(cmd);
}
}
RunWorkerCompletedEventArgs e)
{
//dọn rác sau khi đã gửi xong
((BackgroundWorker)sender).Dispose();
Trang 7GC.Collect();
}
{
Command cmd = (Command)e.Argument;
e.Result = this.SendCommandToClient(cmd);
}
//Sử dụng semaphor để tránh xung đột khi gửi
System.Threading.Semaphore semaphor = new System.Threading.Semaphore(1, 1);
{
try
{
semaphor.WaitOne();
//Type
buffer = BitConverter.GetBytes((int)cmd.CommandType);
this.networkStream.Write(buffer, 0, 4);
this.networkStream.Flush();
//Sender IP
byte[] senderIPBuffer = Encoding.ASCII.GetBytes(cmd.SenderIP.ToString()); buffer = new byte[4];
buffer = BitConverter.GetBytes(senderIPBuffer.Length);
this.networkStream.Write(buffer, 0, 4);
this.networkStream.Flush();
this.networkStream.Write(senderIPBuffer, 0, senderIPBuffer.Length);
this.networkStream.Flush();
//Sender Name
byte[] senderNameBuffer =
Encoding.Unicode.GetBytes(cmd.SenderName.ToString());
buffer = new byte[4];
buffer = BitConverter.GetBytes(senderNameBuffer.Length);
this.networkStream.Write(buffer, 0, 4);
this.networkStream.Flush();
this.networkStream.Write(senderNameBuffer, 0, senderNameBuffer.Length); this.networkStream.Flush();
//Target
byte[] ipBuffer = Encoding.ASCII.GetBytes(cmd.Target.ToString());
buffer = new byte[4];
buffer = BitConverter.GetBytes(ipBuffer.Length);
this.networkStream.Write(buffer, 0, 4);
this.networkStream.Flush();
this.networkStream.Write(ipBuffer, 0, ipBuffer.Length);
this.networkStream.Flush();
//Meta Data
if (cmd.MetaData == null || cmd.MetaData == "")
cmd.MetaData = "\n";
byte[] metaBuffer = Encoding.Unicode.GetBytes(cmd.MetaData);
buffer = new byte[4];
buffer = BitConverter.GetBytes(metaBuffer.Length);
this.networkStream.Write(buffer, 0, 4);
this.networkStream.Flush();
Trang 8this.networkStream.Write(metaBuffer, 0, metaBuffer.Length);
this.networkStream.Flush();
semaphor.Release();
return true;
}
{
semaphor.Release();
return false;
}
}
Bước 10: Tiếp tục viết thêm hàm Disconnect để ngắt kết nối, đóng socket và các
Event
{
if (this.socket != null && this.socket.Connected)
{
try
{
this.socket.Shutdown(SocketShutdown.Both);
this.socket.Close();
return true;
}
catch
{
return false;
}
}
else
return true;
}
{
if (CommandReceived != null)
CommandReceived(this, e);
}
{
if (Disconnected != null)
Disconnected(this, e);
}
Bước 11: Chuyển qua file Server.cs và lập trình các chức năng cơ bản cho 1 server
chat Bao gồm forward tin nhắn, quản lý các client, và các hàm hỗ trợ.
Trước tiên là khai báo các thuộc tính như sau:
Trang 9private IPAddress serverIp;//địa chỉ IP của server
Và Contructor.
{
serverIp = Dns.GetHostAddresses(Dns.GetHostName())[0];
listClient = new List<ClientManager>();
}
Bước 12: Ta viết hàm chạy Server Chú ý server chạy trên Thread độc lập dựa vào
đối tượng BackgroundWorker Có 1 số hàm private kèm theo để quản lý listClient như
thêm, xóa các client khi mất kết nối, login hay logout.
public void Start()
{
bwServer = new BackgroundWorker();
bwServer.DoWork += new DoWorkEventHandler(bwServer_DoWork);
bwServer.RunWorkerAsync();
}
private void bwServer_DoWork(object sender, DoWorkEventArgs e)
{
listenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
ProtocolType.Tcp);
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Any, port);
listenerSocket.Bind(serverEndPoint);
listenerSocket.Listen(200);
while (true)
{
//tao 1 ClientManager mới bất kỳ khi nào có kêt nối mới
ClientManager newClient = new ClientManager(listenerSocket.Accept());
newClient.CommandReceived += new CommandReceivedEventHandler(newClient_CommandReceived); newClient.Disconnected += new DisconnectedEventHandler(newClient_Disconnected);
//xóa client trong list nếu IP của nó đã được lưu trong list
this.RemoveClientManager(newClient.IP);
//thêm Client mới vào list
listClient.Add(newClient);
}
}
//Dọn dẹp, xóa client trong list nếu server mất kêt nối với nó và gửi clientlist mới
private void newClient_Disconnected(object sender, ClientEventArgs e)
{
ClientManager client = (ClientManager)sender;
listClient.Remove(client);
UserEventArg userEvent = new UserEventArg(client.ClientName);
SendUserOnlineList();
}
private void newClient_CommandReceived(object sender, CommandEventArgs e)
{
ClientManager client = (ClientManager)sender;
Command cmd = e.Command;
switch (cmd.CommandType)
{
// khi đang nhập
case CommandType.Login:
{
Trang 10//đặt tên cho Client mới bằng tên đã đăng nhập
string name = SetManagerName(cmd.SenderIP, cmd.MetaData);
//kiểm tra xem tên có trùng với tên đã có không
if (!IsNameExists(cmd.SenderIP, name))
{
//nêu ko thi gửi clientlist mới cho tất cả các máy
SendUserOnlineList();
}
else
{
//nếu có thì gửi thông báo là tên đã có người khác dùng
Command sendcmd = new Command(CommandType.NameExists, cmd.SenderIP);
sendcmd.SenderIP = serverIp;
sendcmd.SenderName = "server";
client.SendCommand(sendcmd);
}
break;
}
//khi đang xuât
case CommandType.Logout:
{
SendUserOnlineList();
break;
}
//forward tin nhắn
case CommandType.Message:
{
string targetname = cmd.MetaData.Split(new char[] { '|' })[0]; SendCommandToTarget(targetname, cmd);
break;
}
}
}
//Đặt tên cho Client
private string SetManagerName(IPAddress remoteClientIP, string nameString)
{
int index = this.IndexOfClient(remoteClientIP);
if (index != -1)
{
string name = nameString.Split(new char[] { ':' })[0];
this.listClient[index].ClientName = name;
return name;
}
return "";
}
//Lấy vị trí của Client cho địa chỉ là IP trong list
private int IndexOfClient(IPAddress ip)
{
int index = -1;
foreach (ClientManager cMngr in listClient)
{
index++;
if (cMngr.IP.Equals(ip))
return index;
}
return -1;
}
//Xóa 1 client có địa chỉ là IP
private bool RemoveClientManager(IPAddress ip)
{
lock (this)
{
int index = this.IndexOfClient(ip);