Professional Documents
Culture Documents
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
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,
using System.Net;
namespace UITChat
1
{
public class Command
{
private IPAddress senderIP;// địa chỉ IP máy gửi
public IPAddress SenderIP
{
get { return senderIP; }
set { senderIP = value; }
}
private string senderName;//tên người gửi
public string SenderName
{
get { return senderName; }
set { senderName = value; }
}
2
Bướ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);
3
get { return ((IPEndPoint)this.socket.RemoteEndPoint).Port; }
}
public ClientEventArgs(Socket clientManagerSocket)
{
this.socket = clientManagerSocket;
}
}
}
4
}
#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 TargerIp
buffer = new byte[ipSize];
readBytes = this.networkStream.Read(buffer, 0, ipSize);
5
if (readBytes == 0)
break;
cmdTarget = System.Text.Encoding.ASCII.GetString(buffer);
//Đọ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.
public void SendCommand(Command cmd)
{
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);
}
}
6
GC.Collect();
}
//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();
7
this.networkStream.Write(metaBuffer, 0, metaBuffer.Length);
this.networkStream.Flush();
semaphor.Release();
return true;
}
catch
{
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 .
public bool Disconnect()
{
if (this.socket != null && this.socket.Connected)
{
try
{
this.socket.Shutdown(SocketShutdown.Both);
this.socket.Close();
return true;
}
catch
{
return false;
}
}
else
return true;
}
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:
private List<ClientManager> listClient;//danh sach các client online
private Socket listenerSocket;//socket để nghe các kết nối mới
private int port = 1024;//port cố định để nhận Command
8
private IPAddress serverIp;//địa chỉ IP của server
private BackgroundWorker bwServer;//Thread chính của server
Và Contructor.
public Server()
{
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();
}
9
//đặ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);
10
if (index != -1)
{
string name = listClient[index].ClientName;
listClient.RemoveAt(index);
SendUserOnlineList();
return true;
}
return false;
}
}
//kiểm tra xem tên đăng nhập đã có chưa
private bool IsNameExists(IPAddress remoteClientIP, string nameToFind)
{
foreach (ClientManager mngr in this.listClient)
if (mngr.ClientName == nameToFind && !mngr.IP.Equals(remoteClientIP))
return true;
return false;
}
Bước 13: Viết 1 số hàm public quan trọng để điều kiển Server.
//gửi command cho tất cả client trong list
public void BroadCastCommand(Command cmd)
{
foreach (ClientManager mngr in this.listClient)
mngr.SendCommand(cmd);
}
//gửi command cho targetname bằng cách tìm ClientManager trong list
public void SendCommandToTarget(string targetname, Command cmd)
{
foreach (ClientManager mngr in this.listClient)
if (mngr.ClientName.Equals(targetname))
{
cmd.Target = mngr.IP;
mngr.SendCommand(cmd);
return;
}
}
//Gửi danh sách client Online cho tất cả các máy online trên mạng
public void SendUserOnlineList()
{
string userOnline = "";
foreach (ClientManager c in listClient)
{
userOnline += "|" + c.ClientName + ":" + c.IP.ToString();
}
Command cmd = new Command(CommandType.ClientList, this.serverIp, userOnline);
cmd.SenderIP = this.serverIp;
cmd.SenderName = "server";
BroadCastCommand(cmd);
}
//đóng Server
public void Stop()
{
foreach (ClientManager c in listClient)
{
c.Disconnect();
}
11
}
Bước 14: Bước cuối cùng để hoàn tất 1 server chat đơn giản. Mở file frmServer.cs.
Về phần giao diện form ta có thể tự do thích vẽ gì cũng được. Quan trọng là xử lý 2 xự
kiện chính cho form đó là Form_Load và Form_Closing. Tạo 1 đối tượng Server. Gọi
hàm Start() ở Form_Load và hàm stop ở Form_Closing. Ta có thể thêm nút Start và
Stop lên form.
Bước 3: Mở file ChatClient.cs, tạo 1 class ChatClient và tiến hành cài đặt cho class
này.
12
private BackgroundWorker bwReceiver;//thread nhận command
private BackgroundWorker bwConnector;//thread kết nối
private NetworkStream networkStream;//Luồn để ghi,đọc dữ liệu
private Socket clientSocket;
Nhận Command, Client cần phải kết nối với Server, tạo socket kết nối với Server rồi
mới thực hiện nhận Command:
public void ConnectTo(IPAddress address)
{
remoteAddress = address;
bwConnector = new BackgroundWorker();
bwConnector.DoWork += new DoWorkEventHandler(bwConnector_DoWork);
bwConnector.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(bwConnector_RunWorkerCompleted);
bwConnector.RunWorkerAsync();
}
private void bwConnector_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
//kiểm tra xem kết nối có thành công không và phát ra xự kiện tương ứng
if ((bool)e.Result == false)
OnConnectingFailed(new EventArgs());
else
OnConnectingSuccessed(new EventArgs());
((BackgroundWorker)sender).Dispose();
}
13
{
try
{
//tạo socket
this.clientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
IPEndPoint serverEP = new IPEndPoint(remoteAddress, port);
//kết nối tới IP của server
this.clientSocket.Connect(serverEP);
e.Result = true;
//tạo 1 luồn để nhận và ghi dữ liệu lên socket
this.networkStream = new NetworkStream(this.clientSocket);
//tạo 1 thread để nhận Command
this.bwReceiver = new BackgroundWorker();
this.bwReceiver.WorkerSupportsCancellation = true;
this.bwReceiver.DoWork += new DoWorkEventHandler(Receive);
this.bwReceiver.RunWorkerAsync();
}
catch
{
e.Result = false;//kết nối ko thành công
}
}
private void Receive(object sender, DoWorkEventArgs e)
{
Tương tự bên phía Server
}
Gửi Command:
public void SendCommandToDestination(Command cmd)
{
if (this.clientSocket != null && this.clientSocket.Connected)
{
BackgroundWorker bwSender = new BackgroundWorker();
bwSender.DoWork += new DoWorkEventHandler(bwSender_DoWork);
bwSender.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(bwSender_RunWorkerCompleted);
bwSender.RunWorkerAsync(cmd);
}
}
private void bwSender_DoWork(object sender, DoWorkEventArgs e)
{
Command cmd = (Command)e.Argument;
e.Result = this.SendCommand(cmd);
}
private void bwSender_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e)
{
((BackgroundWorker)sender).Dispose();
GC.Collect();
}
System.Threading.Semaphore semaphor = new System.Threading.Semaphore(1, 1);
private bool SendCommand(Command cmd)
{
Tương tự như bên Server
}
14
Bước 5: Thiết kế form để Chat.Chuyển qua project UITChatClient. Reference đến 2
project còn lại.
15
this.Close();
}
16
if (s.Trim()!="" && s!=client.UserName)
lsbUserOnline.Items.Add(s);
}
- Tiếp theo ta tạo 1 đối tượng ChatClient, trong Form_Load ta sẽ cho hiện
frmLogin để lấy thông tin, khởi tạo đối tượng ChatClient và kết nối với Server như
sau:
formlogin = new frmLogin();
formlogin.ShowDialog();
if (formlogin.IsCancel)
{
this.Close();
return;
}
try
{
//tạo đối tượng ChatClient
client = new ChatClient(formlogin.UserName);
//gán các xự kiện
client.CommandReceived += new
CommandReceivedEventHandler(client_CommandReceived);
client.ConnectingFailed += new
ConnectingFailedEventHandler(client_ConnectingFailed);
client.Disconnected += new DisconnectedEventHandler(client_Disconnected);
client.ConnectingSuccessed += new
ConnectingSuccessedEventHandler(client_ConnectingSuccessed);
//kết nối với server
client.ConnectTo(IPAddress.Parse(formlogin.IPString));
}
catch
{
MessageBox.Show("Erro!!!");//báo lỗi nếu ip server bị sai
Close();
}
- Tiếp theo ta xử lý các tình huống Disconnected, ConnectingSuccessed,
ConnectedFailed như sau:
private void client_ConnectingSuccessed(object sender, EventArgs e)
{
//gửi tên đăng nhập cho server và thông báo đã login
Command cmd = new Command(CommandType.Login, client.RemoteAddress,
formlogin.UserName);
cmd.SenderIP = client.LocalAddress;
cmd.SenderName = client.UserName;
client.SendCommandToDestination(cmd);
}
17
{
MessageBox.Show("Connecting failed");
this.Close();
}
- Phần quan trọng nhất là xử lý cho tính huống CommandReceive. Đối với mỗi
loại CommandType ta có các cách xử lý khác nhau. Sau đây là cách xử lý cho
3 loại CommandType cơ bản nhất cho chương trình Chat.
Command cmd = e.Command;
string[] metadata = cmd.MetaData.Split(new char[] { '|' });
switch (cmd.CommandType)
{
case CommandType.ClientList:
{
//Cập nhật lại ClientList
if (!this.InvokeRequired)
this.Invoke(new updateUserList(UpdateUserList),new
object[]{metadata});
break;
}
case CommandType.Message:
{
int t = cmd.MetaData.IndexOf('|');
string message = cmd.MetaData.Substring(t+1);
string name = cmd.SenderName;
//gọi hàm InserMessage thông qua delegate
this.Invoke(new insertMessage(InsertMessage), new object[] {
name, message });
break;
}
case CommandType.NameExists:
{
//nếu tên đã tồn tại rùi thì sẽ yêu cầu nhập lại tên khác
MessageBox.Show("Name is Exist");
formlogin = new frmLogin();
formlogin.ShowDialog();
client.UserName = formlogin.UserName;
Command logincmd = new Command(CommandType.Login,
client.RemoteAddress, client.UserName);
logincmd.SenderIP = client.LocalAddress;
logincmd.SenderName = client.UserName;
client.SendCommandToDestination(logincmd);
break;
}
}
- Cuối cùng ta gọi hàm client.Disconnect() trong hàm Form_Closing
18
Bước 7: Tạo thêm 1 form có tên là frmInstantChat.Giao diện và cách đặt tên
tương tự như hình vẽ. Form này
cần có các thuộc tính là
UserName (tên người dùng),
TargetName(tên bạn chat),
MainForm(form chính) và 1
hàm public InsertMessage dùng
để chèn thêm 1 message vào
RichTextBox và có thể gọi từ
form khác.
Cách viết code như sau:
19
}
20