2019独角兽企业重金招聘Python工程师标准>>>
实验功能:
设计程序,分别构建通信的两端:服务器端和客户端应用程序,套接字类型为面向连接的Socket,自己构建双方的应答模式,实现双方的数据的发送和接收(S发给C,C发给S)。
服务端程序能响应单个或任意多个客户端连接请求;服务端能向单个客户发送消息,支持群发消息给所有客户端;
通信的双方具备异常响应功能,包括对方异常退出的处理。如果客户端退出,服务器有响应;反之亦然。
客户端之间直接通信,C与C之间直接通信(不是通过S传递)。
设计思路:
服务器设计思路:服务器的设计是这次实验最复杂的部分,因为服务器的功能比较多。作为服务器,它要可以同时与多个客户端连接,为每一个连接的客户端创建一个通信Socket,自己还要有一个Socket用于监听客户端的连接请求;服务器要创建一个数据结构用于保存连接进来的客户端的信息(Socket和客户端的名字);服务器要将连接进来的客户端显示出来,用户可以根据显示出来的用户列表来向指定的客户端发信息;服务器要能及时地刷新客户端列表,当有新的客户端连接进来或是退出的时候要及时通知所有的客户端并刷新自己的客户端列表;服务器要能接收所有的客户端的信息,并将信息无错地转发给指定的客户端。
客户端设计思路:客户端的设计相对于服务器来说的话对会比较简单一点。客户端要有接收服务器信息的功能,但客户端只向服务器发信息,客户端通过服务器的转发功能向其它的客户端发送信息。客户端要可以处理服务器发过来的信息,还要有数据结构用来保存所有客户端的名字,并将所有客户端名字列表显示出来。可以指定客户端列表里面的多个项来向不同的客户端发信息。
通信数据处理:无论是服务器发给客户端,还是客户端发给服务器的数据,双方都要进行处理。对于不用的类型的数据要设计不用的标志信息,当双方收到信息后跟据标志信息进行不同的处理。数据可以分为三种 :
a)登陆信息。这类信息提示有新的客户端连接进来。该信息由客户端首先发给服务器,服务器收到后会更新自己的在线客户端列表,增加与该客户端通信的Socket和名字,并将该信息转发给所有在线的客户端,提醒客户端即时更新客户端列表。这类信息以“login,客户端名”的形式发送。
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(); // 关闭Socketusercount = 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); //获得计算机IPforeach(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信息:
接收的信息是其它客户端直接发过来的,不经过服务器的转发。
客户端退出时:
客户端退出时,服务器会知道退出的用户,并把该客户端移出列表,同时发信息通知其它的客户端,使它们可以及时地更新用户列表。
服务器退出时:
当服务器退出时,所有的客户端会提示与服务器失去联系,并将在线用户列表清空。