好久没有来了,公司项目差不多结束了,这个简单应用也应该结束。
本篇会把我写的这个工程中核心代码讲解一下,并附有程序下载:p2p.rar
首先从服务器说起。P2P也需要服务器,而且服务器不能至于nat中,或是在局域网中。当然也是有办法的,如果要在局域网中测试,可以在路由中设置一个DMZ主机,也就是设置一个IP的机器为类外网机器,可以直接被外网访问。这样再运行服务器就可以了。其他内网中的客户端就可以用服务器的外网IP来登录。
下面是服务器界面:
服务器代码较简单,主要实现以下几个功能:
1. 保存客户端列表信息,当有新客户端登录时或登出时将列表发送给所有客户端。
2. 如果需要,帮助通知客户端进行UDP打洞,特别是发送文件时。
以下是主要代码:
1
public class UDPServer2

{3
private UdpClient udpser;//服务器监听UDP4
public UserCollection userList;//客户端列表5
private Thread serverThread;//监听线程6
private IPEndPoint remotePoint;//保存一远程终端信息7

8
public UDPServer()//构造函数9

{10
this.userList = new UserCollection();11
this.remotePoint = new IPEndPoint(IPAddress.Any, 0);12
}13

14
public void Start()15

{16
try17

{18
this.udpser = new UdpClient(2000);//监听端口是2000,无所谓什么,不要小于1024就行19
this.serverThread = new Thread(new ThreadStart(Run));20
this.serverThread.Start();//启动监听线程21
}22
catch (Exception exp)23

{24
Console.WriteLine("Start P2P Server error: " + exp.Message);25
}26
}27

28
public void Stop()29

{30
try31

{32
this.serverThread.Abort();33
this.udpser.Close();34
}35
catch (Exception exp)36

{37
Console.WriteLine("Stop error: " + exp.Message);38
}39
}40

41
private void Run()//监听线程函数42

{43
byte[] buffer = null;44
while (true)45

{46
byte[] msgBuffer = this.udpser.Receive(ref remotePoint);//如果没有接收到信息,就会停在这里47
try48

{49
object msgObj = FormatterHelper.Deserialize(msgBuffer);//反序列化信息50
Type msgType = msgObj.GetType();51
if (msgType == typeof(LoginMessage))//是登录信息52

{53
LoginMessage lginMsg = (LoginMessage)msgObj;54
Console.WriteLine("has an user login: {0}", lginMsg.UserName);55
IPEndPoint userEndPoint = new IPEndPoint(remotePoint.Address, remotePoint.Port);56
User user = new User(lginMsg.UserName, userEndPoint);57
userList.Add(user);//添加新登录客户端58
GetUsersResponseMessage usersMsg = new GetUsersResponseMessage(userList);59
buffer = FormatterHelper.Serialize(usersMsg);//序列化信息便于发送60
foreach (User us in userList)61

{62
this.udpser.Send(buffer, buffer.Length, us.NetPoint);//将客户列表发给客户端63
}64
}65
else if (msgType == typeof(LogoutMessage))//是登出信息66

{67
LogoutMessage lgoutMsg = (LogoutMessage)msgObj;68
P2PHelper.User lgoutUser = userList.Find(lgoutMsg.UserName);69
if (lgoutUser != null)70

{71
userList.Remove(lgoutUser);//删除客户端信息72
}73
foreach (User us in userList)74

{75
this.udpser.Send(buffer, buffer.Length, us.NetPoint);//将客户列表发给客户端76
}77
}78
else if (msgType == typeof(AskPunchHoleMessage ))//是要求打洞信息,通过服务器通知进行UDP打洞79

{80
Console.WriteLine("SendFileClientMessage");81
AskPunchHoleMessage askPunchHoleMessage = (AskPunchHoleMessage )msgObj;82
PunchHoleMessage punchHoleOneMsg = new PunchHoleMessage(remotePoint);83
buffer = FormatterHelper.Serialize(punchHoleOneMsg);84
this.udpser.Send(buffer, buffer.Length, askPunchHoleMessage.RemotePoint);//通知需要打洞的客户端往哪打洞85
}86
}87

catch
{ }88
}89
}90
}
这样一个简单的服务器就完成了。下面介绍客户端的代码,比较复杂。
客户端界面:
客户端主要实现以下几个功能:
1. 登录或登出服务器,与服务器进行通信。
2. 与客户端通信,包括发送消息和发送文件。
以下是主要代码:
public class UDPClient
{
private UdpClient client;//保持监听的UDP客户端
public IPEndPoint hostPoint;//保存服务器终端信息
private IPEndPoint remotePoint;//保存某一终端信息
public UserCollection userList;//客户端列表
private string Name;//用户名
private string Password;//密码
private Thread listenThread;//监听线程
private const int MAXTRY = 3;//最大要求打洞次数
public UDPClient()
{
this.remotePoint = new IPEndPoint(IPAddress.Any, 0);
this.client = new UdpClient();
this.userList = new UserCollection();
}
public void Start()
{
this.listenThread = new Thread(new ThreadStart(Run));
this.listenThread.Start();
}
public bool ConnectToServer()
{
try
{
LoginMessage lginMsg = new LoginMessage(this.Name, this.Password);
byte[] buffer = FormatterHelper.Serialize(lginMsg);//序列化登录信息,便于发送
client.Send(buffer, buffer.Length, hostPoint);
buffer = client.Receive(ref remotePoint);//等待接收服务器发送来的客户端列表
GetUsersResponseMessage srvResMsg = (P2PHelper.GetUsersResponseMessage)P2PHelper.FormatterHelper.Deserialize(buffer);
userList.Clear();
foreach (P2PHelper.User user in srvResMsg.UserList)
{
userList.Add(user);//添加客户端
}
return true;
}
catch
{
return false;
}
}
public bool CheckServer()//检查是否能与服务器通信,这是很必要的,我发现,vista系统或是一些nat下的机器,每过差不多两分钟便不能再与服务器通信,也就是这个端口在路由上已经被关闭,所以,我设定,每分钟与与服务器通信一次,以保持端口正常开启, 代码与CheckClient前半部分相似,不再详述
{
}
public bool CheckClient(string toUserName)//检查是否能与客户端通信,如果不能,则要求服务器通知其打洞到这里,这是保证通信必须要经过的步骤
{
P2PHelper.User toUser = userList.Find(toUserName);
if (toUser == null)
{
return false;
}
for (int i = 0; i < MAXRETRY; i++)
{
CheckMessage checkMsg = new CheckMessage (this.Name);
byte[] buffer = FormatterHelper.Serialize(checkMsg );
client.Send(buffer, buffer.Length, toUser.NetPoint);
for (int j = 0; j < 5; j++)
{
if (this.ReceivedACK)
{
this.ReceivedACK = false;
return true;
}
else
{
Thread.Sleep(100);
}
}
AskPunchHoleMessage askPunchHoleMsg = new AskPunchHoleMessage (Name, toUserName);
buffer = P2PHelper.FormatterHelper.Serialize(askPunchHoleMsg );
client.Send(buffer, buffer.Length, hostPoint);
}
return false;
}
public bool SendMessageToRemote(IPEndPoint remote, PPMessage ppm)//发送消息到客户端,主要是聊天消息,其它消息不再详述
{
byte[] buffer = new byte[1024];
bool shouldSend = true;
switch (index)
{
case 1: ChatMessage chatMsg = (ChatMessage)ppm;
buffer = FormatterHelper.Serialize(chatMsg);
break;
}
if (!shouldSend)
{
return false;
}
client.Send(buffer, buffer.Length, remote);
return true;
}
public void Dispose()//注销,通知服务器
{
try
{
P2PHelper.LogoutMessage lgoutMsg = new P2PHelper.LogoutMessage(myName);
byte[] buffer = FormatterHelper.Serialize(lgoutMsg);
client.Send(buffer, buffer.Length, hostPoint);
if (this.listenThread != null)
{
this.listenThread.Abort();
this.listenThread = null;
}
}
catch
{ }
}
private void Run()//监听线程函数,与服务器差不多,只是需要多监听几种消息,不再详述
{
}
}
客户端的重点是发送文件,所以,如何发送完整,并接收完整是关键,下面是发送和接收的过程,主要是实现如何不丢失:
public void SendFile()//发送文件函数
{
FileStream fs = new FileStream(this.filename, FileMode.Open, FileAccess.Read);//文件流
try
{
byte[] fileBuffer = new byte[1024]; // 每次传1KB
FileMessage fileMsg;//发送文件消息
AnswerFileMessage answerFileMsg;//回应文件消息
byte[] buffer = new byte[1024];
int bytesRead = 1;
do
{
buffer = fileClient.Receive(ref remotePoint);//同样需要接收消息
object msgObj = FormatterHelper.Deserialize(buffer);
Type msgType = msgObj.GetType();
if (msgType == typeof(AnswerFileMessage))//判断是AnswerFileMessage就继续发送,否则等待
{
answerFileMsg = (AnswerFileMessage)msgObj;
if (this.sendCount == answerFileMsg.Length)//发送前判断接收方接收的数据是否完整,如果完整就继续发送
{
bytesRead = fs.Read(fileBuffer, 0, fileBuffer.Length);//获得要发送的字节数
if (bytesRead > 0)//判断字节数
{
fileMsg = new P2PHelper.FileMessage("", fileBuffer, bytesRead);
buffer = P2PHelper.FormatterHelper.Serialize(fileMsg);
this.fileClient.Send(buffer, buffer.Length, this.remotePoint);
this.totalBytes += bytesRead;
this.sendCount++;
}
else//说明已经发完,则发送发完消息
{
P2PHelper.EndSendFileMessage endFileMsg = new P2PHelper.EndSendFileMessage();
buffer = P2PHelper.FormatterHelper.Serialize(endFileMsg);
fileClient.Send(buffer, buffer.Length, this.remotePoint);
break;
}
}
else//如果不完整,则将之前发送的再发送一遍
{
fileMsg = new FileMessage("", fileBuffer, bytesRead);
buffer = FormatterHelper.Serialize(fileMsg);
this.fileClient.Send(buffer, buffer.Length, this.remotePoint);
}
Console.WriteLine("send "+this.totalBytes);
}
else if (msgType == typeof(P2PHelper.EndSendFileMessage))
{
break;
}
} while (true);
}
catch (Exception ex)
{
Console.WriteLine("Send file has lost
");}
finally
{
}
}
public void ReceiveFile()
{
FileStream fs = new FileStream(this.filename, FileMode.CreateNew, FileAccess.Write);//文件流
try
{
P2PHelper.FileMessage fileMsg;
byte[] buffer;
byte[] fileBuffer = new byte[1024];
int bytesRead = 1;
buffer = FormatterHelper.Serialize(new P2PHelper.AnswerFileMessage(0));//先发送一个AnswerFileMessage通知发送方开始发送
fileClient.Send(buffer, buffer.Length, this.remotePoint);
do
{
buffer = fileClient.Receive(ref remotePoint);//接收消息
object msgObj = FormatterHelper.Deserialize(buffer);
Type msgType = msgObj.GetType();
if (msgType == typeof(FileMessage))//判断FileMessage
{
fileMsg = (FileMessage)msgObj;
bytesRead = fileMsg.Length;
fs.Write(fileMsg.Message, 0, fileMsg.Length);//将接收到信息写入文件
this.totalBytes += bytesRead;
this.sendCount++;//统计接收次数是否完整,发送给发送方验证
buffer = FormatterHelper.Serialize(new P2PHelper.AnswerFileMessage(this.sendCount));
this.fileClient.Send(buffer, buffer.Length, this.remotePoint);//发送AnswerFileMessage通知发送方继续发送
}
else if (msgType == typeof(EndSendFileMessage))//判断结束发送消息,则结束接收
{
break;
}
} while (true);
}
catch (Exception ex)
{
Console.WriteLine("Receive file has lost
");}
finally
{
}
}
这些就是客户端的主要代码,再加上用于界面操作等代码就可以运行了,快试试吧。
浙公网安备 33010602011771号