实验功能:c#
设计程序,分别构建通讯的两端:服务器端和客户端应用程序,套接字类型为面向链接的Socket,本身构建双方的应答模式,实现双方的数据的发送和接收(S发给C,C发给S)。服务器
服务端程序能响应单个或任意多个客户端链接请求;服务端能向单个客户发送消息,支持群发消息给全部客户端;数据结构
通讯的双方具有异常响应功能,包括对方异常退出的处理。若是客户端退出,服务器有响应;反之亦然。spa
客户端之间直接通讯,C与C之间直接通讯(不是经过S传递)。线程
设计思路:设计
服务器设计思路:服务器的设计是此次实验最复杂的部分,由于服务器的功能比较多。做为服务器,它要能够同时与多个客户端链接,为每个链接的客户端建立一个通讯Socket,本身还要有一个Socket用于监听客户端的链接请求;服务器要建立一个数据结构用于保存链接进来的客户端的信息(Socket和客户端的名字);服务器要将链接进来的客户端显示出来,用户能够根据显示出来的用户列表来向指定的客户端发信息;服务器要能及时地刷新客户端列表,当有新的客户端链接进来或是退出的时候要及时通知全部的客户端并刷新本身的客户端列表;服务器要能接收全部的客户端的信息,并将信息无错地转发给指定的客户端。code
客户端设计思路:客户端的设计相对于服务器来讲的话对会比较简单一点。客户端要有接收服务器信息的功能,但客户端只向服务器发信息,客户端经过服务器的转发功能向其它的客户端发送信息。客户端要能够处理服务器发过来的信息,还要有数据结构用来保存全部客户端的名字,并将全部客户端名字列表显示出来。能够指定客户端列表里面的多个项来向不一样的客户端发信息。orm
通讯数据处理:不管是服务器发给客户端,仍是客户端发给服务器的数据,双方都要进行处理。对于不用的类型的数据要设计不用的标志信息,当双方收到信息后跟据标志信息进行不一样的处理。数据能够分为三种 :事件
a)登录信息。这类信息提示有新的客户端链接进来。该信息由客户端首先发给服务器,服务器收到后会更新本身的在线客户端列表,增长与该客户端通讯的Socket和名字,并将该信息转发给全部在线的客户端,提醒客户端即时更新客户端列表。这类信息以“login,客户端名”的形式发送。ip
b)退出信息。这类信息提示发信息的客户端即将退出服务器。该信息由客户端首先发给服务器,服务器收到后会更新本身的在线客户端列表,删除与该客户端通讯的Socket和名字,并将该信息转发给全部在线的客户端,提醒客户端即时更新客户端列表。这类信息以“logout,客户端名”的形式发送。
c)通讯信息。这类信息提示发送信息的客户端向在线的某个客户端或是服务器发起了通讯,也能够是服务器与某个客户端发起了通讯。若是该信息是服务器发给客户端或是客户端发给服务器,则直接发送,不用通过转发;若是是客户端向另外一个客户端发送信息,则是先发给服务器,服务再转发给指定的客户端。这类信息以“talk,目的客户端名,发送的信息”的形式发送。
线程的设计思路:在服务器方面,须要一个程专门用于监听客户端的链接请求,对于链接进来的每个客户端,还要建立一个线程用于接收信息,程序的主线程用于向不一样的客户端发送信息,因此服务器至少须要要n+2(n>=0)个线程;在客户端方面,须要一个线程用于接收服务的信息,还要一个线程用于向服务器发送信息,因此只须要2个线程。
信息无边界问题:因为这里用的C#里面原始Socket套接字,因此在数据收发的过程当中会出现无边界的问题。有时服务器向客户端发送多条不一样类型的信息,客户端会把它们合并在一块儿,当成一条信息处理。为了提取不一样类型的信息,发送信息以前要为每一条信息加特定的结束符。
客户端之间直接通讯问题:为了实现客户端之间的直接通讯,客户端之间必须知道其它客户端的IP和端口,这能够经过服务器的转发获得客户端之间的IP和端口。客户端也必须有一个本身可用的端口号用来和其它客户端之间的通讯,因此除了第一次的客户端与服务器的链接之外,客户端便是服务器也是客户端。
服务器处理不一样类型信息代码:
string[] splitString = receiveString.Split(','); //分割字符 switch (splitString[0].ToLower()) { case "login": // 登录信息 user.username = splitString[1]; userList.Add (user); // 增长用户列表 AddItemToListBox (user.username); // 刷新用户列表 sendToAllClient (user,receiveString); // 通知全部在线用户 FirstLogin (user); break; case "logout": // 退出信息 DeletItemInListBox (user.username); sendToAllClient (user,receiveString);// 通知全部在线用户 RemoveUser (user); // 删除用户信息 UserCount (--usercount); // 刷新用户列表 break; case "talk": // 对话信息 multMessage (user,receiveString); // 转发对话 break; default: sendMessageTorichBox ("不知道什么意思!"); break; }
服务器监听客户端代码:
private void button1_Click(object sender, EventArgs e) { isNormalExit = false; buttu_richBoxDelegate d = buttu_richBox; // 委托事件 try { myListener.Listen (10); // 开始监听 richTextBox1.Invoke(d,"成功监听."); // 成功监听 } catch{ richTextBox1.Invoke(d,"监听失败。"); } Thread mhThread = new Thread(ListenClientConnect); // 建立新的线程 mhThread.IsBackground = true; // 设置为后台线程 mhThread.Start (); button1.Enabled=false; // 开始监听按钮不可用 button2.Enabled= true; }
服务器接受客户端代码:
private void ListenClientConnect () { Socket newClient =null; While (isNormalExit==false) { try { newClient = myListener.Accept(); // 接受客户端 if(isNormalExit == true) // 若是服务器中止监听 { newClient.Close(); // 关闭Socket usercount = 0; UserCount(usercount); Break; } }Catch{ break; } User user = new User(newClient); // 保存客户端列表 Thread threadReceive = new Thread(ReceiveData); // 建立新的线程 threadReceive.IsBackground=true; //设置为后台线程 threadReceive.Start(user); // 开始线程 UserCount(++usercount); // 客户端人数加1 } }
客户端链接服务器代码:
Private void button1_Click(object sender, EventArgs e) { button1.Enabled = false; client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //新建套接字 AddrichTextBox1Massage d = sendrichTextBox1Massage; Try { String name = Dns.GetHostName(); // 得到计算机的名字 IPHostEntry me = Dns.GetHostEntry (name); //得到计算机IP foreach(IPAddress ips in me.AddressList) { Try { IPEndPoint ep = new IPEndPoint(ips, 8889); client.Connect(new IPEndPoint(ips, 8889)); // 链接服务器 break; } catch {//若获取的IP是vs6的话 } } client.Send(Encoding.UTF8.GetBytes("login," + textBox1.Text));//向服务器发信息 Thread threadReceive = new Thread(new ThreadStart(ReceiveData));//建立新线程 threadReceive.IsBackground = true; // 设置为后台线程 threadReceive.Start(); //开始线程 }
客户端接受服务器信息代码:
private void ReceiveData() { AddrichTextBox1Massage d = sendrichTextBox1Massage; int receiveLength; while(isExit==false) { try{ receiveLength = client.Receive(result); //开始接收信息 recieveMessage=Encoding.UTF8.GetString(result,0,receiveLength); }catch{ if (isExit == false){ richTextBox1.Invoke(d, "与服务器失去联系。"); client.Shutdown(SocketShutdown.Both); // 关闭套接字 client.Close(); } break; } string[] splitString = recieveMessage.Split(','); //处理信息 string command = splitString[0].ToLower(); switch(command) { case "login":AddOnline(recieveMessage); // 登录信息 break; case "logout": RemoveUserName(splitString[1]); // 退出信息 break; case "talk": richTextBox1.Invoke(d, "["+splitString[1] + "]对我说: " + splitString[2]); // 对话信息 break; default: richTextBox1.Invoke(d,"不知什么意思。"); break; } } LostConnect(); //关闭链接 }
客户端监听其它客户端代码:
private void ServerReceive(Object client) { AddrichTextBox1Massage d = sendrichTextBox1Massage; Socket myClientSocket = (Socket)client; byte[] str =new byte[1024]; while (true) { try { int n = myClientSocket.Receive(str); richTextBox1.Invoke(d, Encoding.UTF8.GetString(str, 0, n)); break; } catch { myClientSocket.Close(); //richTextBox1.Invoke(d, "接收消息失败!"); break; } } myClientSocket.Close(); }
程序运行效果:
服务器运行界面:
有客户端链接进服务器:
在线客户列表显示了链接进的客户端的名字,在线客户人数显示为3人
上图表示有3个客户端链接进了服务器。
服务器向客户端发送信息:
服务器向在线客户列表里的2个客户同时发了信息,2个客户端收到了正确的信息。
客户端的启动界面:
客户端自动生成用户的名字。
客户端登录的界面:
客户端显示链接成功,并刷新在线用户列表。
多个客户端链接服务器时的界面:
当有多个客户端与服务器链接时,客户端会自动更新在线用户列表。
客户端向其它客户端发TCP信息:
客户端能够同时向服务器和多个客户端发送信息。
客户端接收来自其它客户端的TCP信息:
接收的信息是其它客户端直接发过来的,不通过服务器的转发。
客户端退出时:
客户端退出时,服务器会知道退出的用户,并把该客户端移出列表,同时发信息通知其它的客户端,使它们能够及时地更新用户列表。
服务器退出时:
当服务器退出时,全部的客户端会提示与服务器失去联系,并将在线用户列表清空。