网络编程之Socket的TCP协议实现客户端与客户端之间的通信
2015-05-18 15:33 糯米粥 阅读(10873) 评论(12) 编辑 收藏 举报我认为当你学完某个知识点后,最好是做一个实实在在的小案例。这样才能更好对知识的运用与掌握
如果你看了我前两篇关于socket通信原理的入门文章。我相信对于做出我这个小案列是完全没有问题的!!
既然是小案列。就难免会有很多不足,所以还请大家见谅。先说下用此案例实现的功能
利用Socke的TCP协议实现了
1:端与端之间的通信(客户端和客户端)包括发送文字,小文件,抖动效果
2:同步实现好友下线与上线的效果,即有其他客户端上线会通知其他已经在线的客户端
3:实现自定义通信协议
服务器窗体布局
布局很明了。顶部一个DataGridView显示连接的客户端信息,来一个就新增一个。走一个就干掉一个。
下面就是显示服务器信息
客户端布局:
窗口布局是不是很大众呢。QQ啊。飞秋。类似的布局。
好了。现在启动服务器。连接两个客户端试试
服务器已经获取了客户端信息
客服端用户列表是空的?因为这里我的处理是不能跟自己聊天所以列表是不显示自己的ip的
那么在登陆一个客户端呢?
服务器
此时客户端都已经获取了对方上线的ip
此时就可以通信了。那来试试上面说的功能。发送文字,抖动,和图片
发送文字:
箭头的方向是发送的开始,反过来发送也是可以的
当有人下线,客户端会更新 "用户列表",同时服务器也会端口对下线者的通信
比如:当端口号为:6477下线。即端口连接
刚下线一个客户端。所以现在登陆一个客户端
发送文件: 这里仅仅是小文件。显示发送一个图片
当选择文件文件后。文件路径会显示在框中
从图中可以看出。是一个图片。名字是 2
单击发送。接收的用户会提示是否接受该文件。
当单击是的时候。则弹出保存对话框
从图片中可以看出来,这里获取到了文件名称。那么客户端是怎么获取到的呢?在下面的代码中会一一讲解
现在重新命名 new.jpg保存看看。已经成功了
抖动效果
为了让大家看到抖动效果。这里放一个gif图片。其实跟QQ抖动类似。当然没他那么优美。那么好看。
看看一个通信的流程图
图片中红色的标记框就是
首先:我第一次登陆成功,服务器发送在线好友给我,显示在用户列表中
同时通知其他在线用户。用人(我)上线了。
当然。当有人上线。服务器也会通知我。然后把上线的ip添加到用列表中
当我下线的时候。通知服务器我下线了。服务器则通知其他在线用户。更新列表
发送消息也是先发送到服务器。然后服务器在转发。
好了。具体功能就是这些了。带着这些实现的功能读下面的代码。思绪应该会比较清晰
还记得创建服务器的三个步骤吗?
第一:创建监听的socket
第二:绑定监听用个的ip和端口号
第三:开始监听
因为监听和获取消息都是要循环的。要么用多线程。要么用socket异步。所以这两个方法要独立出来
创建StartServer()方法。用来开启服务器
1 /// <summary> 2 /// 打开服务器 3 /// </summary> 4 void StartServer() 5 { 6 try 7 { 8 string _ip = tbIp.Text; 9 int _point = int.Parse(tbPoint.Text); 10 11 //创建监听客户端请求的socket 12 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 13 //监听用的ip和端口 14 IPAddress address = IPAddress.Parse(_ip); 15 IPEndPoint point = new IPEndPoint(address, _point); 16 17 //绑定 18 socket.Bind(point); 19 socket.Listen(10); 20 21 22 //异步 开始监听 23 socket.BeginAccept(new AsyncCallback(Listen), socket); 24 25 26 //禁用当前按钮 27 btnStart.Enabled = false; 28 29 //启动时间 30 startTime.Text = DateTime.Now.ToString(); 31 32 //底部提示消息 33 tssMsg.Text = "服务器已经启动"; 34 } 35 catch (Exception ex) 36 { 37 MessageBox.Show(ex.Message); 38 } 39 40 }
从代码可以看出来。我用到了异步通信 BeginAccept方法 回掉方法是Listen
Listen方法即上面说的要循环监听的代码
Listen方法
1 /// <summary> 2 /// 开始监听 3 /// </summary> 4 /// <param name="result"></param> 5 void Listen(IAsyncResult result) 6 { 7 8 try 9 { 10 //获取监听的socket 11 Socket clientSocket = result.AsyncState as Socket; 12 //与服务器通信的socket 13 Socket connSocket = clientSocket.EndAccept(result); 14 15 string ip = connSocket.RemoteEndPoint.ToString(); 16 //连接成功。保存信息 17 if (!Common.connSocket.ContainsKey(ip)) 18 Common.connSocket.Add(ip, connSocket); 19 20 //连接成功,更新服务器信息 21 changeList(connSocket); 22 23 24 //等待新的客户端连接 ,相当于循环调用 25 clientSocket.BeginAccept(new AsyncCallback(Listen), clientSocket); 26 27 byte[] buffer = new byte[1024 * 1024]; 28 29 30 //接收来自客户端信息 ,相当于循环调用 31 connSocket.BeginReceive(Common.ReceiveBuffer, 0, Common.ReceiveBuffer.Length, 0, new AsyncCallback(Receive), connSocket); 32 33 //用户第一次登陆。获取所有在线用户 如果有好友功能。则获取他的好友 34 SendesClient(connSocket); 35 } 36 catch (Exception ex) 37 { 38 39 //MessageBox.Show(ex.Message); 40 } 41 42 43 }
如果你在想为什么Listen方法要这样定义:
void Listen(IAsyncResult result)
你可以去看下多线程,异步委托方面的知识
我博客也有这方面的入门:http://www.cnblogs.com/nsky/p/4425286.html
这里把连接的客服端保存在字典中
key:当前的ip value :当前的socke通信
//连接成功。保存信息 if (!Common.connSocket.ContainsKey(ip)) Common.connSocket.Add(ip, connSocket);
当客户端连接。就获取所有在线的ip
//用户第一次登陆。获取所有在线用户 如果有好友功能。则获取他的好友 SendesClient(connSocket);
SendesClient方法
1 /// <summary> 2 /// 把上线的人发送到客户端 3 /// </summary> 4 /// <param name="connSocket">当前连接的客户端</param> 5 void SendesClient(Socket connSocket) 6 { 7 //自定义协议:[命令 2位] 8 /* 9 * 第一位:10代表是首次登陆获取所有好友,把自己的ip放最后一位 10 * 好像这里默认已经是最后一位了?? 11 */ 12 string key = connSocket.RemoteEndPoint.ToString(); 13 //把自己的ip删除 14 if (Common.connSocket.ContainsKey(key)) 15 Common.connSocket.Remove(key); 16 17 //把自己的key添加到最后一位 18 if (!Common.connSocket.ContainsKey(key)) 19 Common.connSocket.Add(key, connSocket); 20 21 //发送到客户端 22 byte[] clientByte = Encoding.UTF8.GetBytes(string.Join(",", Common.connSocket.Keys)); 23 24 25 //List<byte> bbb = new List<byte>(); 26 //bbb.Add(1); 27 //bbb.AddRange(clientByte); 28 29 List<byte> li = clientByte.ToList(); 30 li.Insert(0, 10);//第一位插入10 代表是获取好友 31 32 //把当前在线ip发送给自己 33 connSocket.Send(li.ToArray()); 34 35 //告诉其他在线的用户。我上线啦,求勾搭 36 //var online = from onn in Common.connSocket 37 // where !onn.Key.Contains(connSocket.RemoteEndPoint.ToString()) //筛选,不包含自己的ip 38 // select onn; 39 40 //if (online.Count() <= 0) return; //当前没有上线的 41 42 foreach (KeyValuePair<string, Socket> item in Common.connSocket) 43 { 44 //不需要给自己发送。因为当自己上线的时候。就已经获取了在线的ip 45 if (item.Key == connSocket.RemoteEndPoint.ToString()) continue; 46 //多线程通知在线用户。 47 Thread thread = new Thread(() => 48 { 49 byte[] buffer = Encoding.UTF8.GetBytes(connSocket.RemoteEndPoint.ToString()); 50 List<byte> list = buffer.ToList(); 51 //有人上线 52 //[命令(12)| ip(上线的ip)| ...] 53 list.Insert(0, 12);//说明有人上线 54 item.Value.Send(list.ToArray()); 55 }); 56 thread.IsBackground = true; 57 thread.Start(); 58 } 59 60 //判断当前用户是否还处于连接状态 61 //if (Common.connSocket.ContainsKey(connSocket.RemoteEndPoint.ToString())) 62 //connSocket.Send(buffer); 63 64 //有人下线。就通知所有在线的人数 65 //如果是qq我想应该是通知我的好友。不是知道是不是 66 67 /* 68 * 如果在线有 A B C 69 * 那么A下线 70 * 是不是要通知B C 71 * 还是在B C 定时访问服务器来获取在线人数呢? 72 * 待解决 73 */ 74 }
那么Listen方法是怎么循环监听的呢?其实就是在Listen里面循环调用自己
//等待新的客户端连接 ,相当于循环调用 clientSocket.BeginAccept(new AsyncCallback(Listen), clientSocket);
监听有客服端连接成功。就开始接受客户端信息
1 //接收来自客户端信息 ,相当于循环调用 2 connSocket.BeginReceive(Common.ReceiveBuffer, 0, Common.ReceiveBuffer.Length, 0, new AsyncCallback(Receive), connSocket);
Common.ReceiveBuffer是什么??
这是我把公共的数据封装在了一个Common类里面。下面会给出
循环监听有了。那么写一个循环接收消息的也就很简单了。依葫芦画瓢
Receive方法:
1 /// <summary> 2 /// 接收来自客户端信息 3 /// </summary> 4 /// <param name="result"></param> 5 void Receive(IAsyncResult result) 6 { 7 8 //与客户端通信的socket 9 Socket clientSocket = result.AsyncState as Socket; 10 11 try 12 { 13 //获取实际的长度值 14 int num = clientSocket.EndReceive(result); 15 if (num > 0) 16 { 17 byte[] data = new byte[num]; 18 //复制实际的长度到data字节数组中 19 Array.Copy(Common.ReceiveBuffer, 0, data, 0, num); 20 21 //判断协议位 22 int command = data[0]; 23 24 //获取内容 25 string source = Encoding.UTF8.GetString(data, 1, num - 1); 26 27 //获取接收者的ip 28 string receiveIp = source.Split(',')[0]; 29 30 31 if (command == 1) //说明发送的是文字 32 { 33 /*协议: 34 * //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 35 */ 36 //获取接收者通信连接的socket 37 if (Common.connSocket.ContainsKey(receiveIp)) 38 { 39 Common.connSocket[receiveIp].Send(data); 40 } 41 } 42 else if (command == 0) //说明是发送的文件 43 { 44 /*协议: 这里50位不知道是否理想。 45 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30位)|响应(文件内容) | ...] 46 */ 47 48 //获取接收者通信连接的socket 49 if (Common.connSocket.ContainsKey(receiveIp)) 50 { 51 Common.connSocket[receiveIp].Send(data); 52 } 53 } 54 else if (command == 2)//抖动一下 55 { 56 //协议 57 //震动 58 //[命令(2)| 对方的ip和自己的ip 50位| ...] 59 60 //获取接收者通信连接的socket 61 if (Common.connSocket.ContainsKey(receiveIp)) 62 { 63 Common.connSocket[receiveIp].Send(data); 64 } 65 } 66 67 //string msg = Encoding.UTF8.GetString(data); 68 //MessageBox.Show(msg); 69 70 71 //接收其他信息 72 clientSocket.BeginReceive(Common.ReceiveBuffer, 0, Common.ReceiveBuffer.Length, 0, new AsyncCallback(Receive), clientSocket); 73 74 } 75 else //客户端断开 76 { 77 clientOff(clientSocket); 78 } 79 } 80 catch (Exception ex) 81 { 82 clientOff(clientSocket); 83 } 84 85 }
当客户端端口会执行clientOff方法。用来通知其他用户。更新列表
clientOff方法
1 /// <summary> 2 /// 客户端关闭 3 /// </summary> 4 void clientOff(Socket clientSocket) 5 { 6 //从集合删除下线的ip 7 string outIp = clientSocket.RemoteEndPoint.ToString(); 8 if (Common.connSocket.ContainsKey(outIp)) 9 Common.connSocket.Remove(outIp); 10 11 //更新服务器在线人数 12 changOnlineCount(false); 13 14 this.Invoke(new Action(() => 15 { 16 //更新列表 17 //删除退出的ip 18 for (int i = 0; i < dgvList.Rows.Count; i++) 19 { 20 if (dgvList.Rows[i].Tag.ToString() == outIp) 21 { 22 dgvList.Rows.RemoveAt(i); 23 break; 24 } 25 } 26 })); 27 28 clientSocket.Shutdown(SocketShutdown.Receive); 29 clientSocket.Close(); 30 31 //通知所有在线用户。有人下线了。需要更新列表,如果是qq是通知我的好友。不知道是不是这样 32 /*这里有点疑问: 33 * 是客户端定时到服务器获取在线用户? 34 * 还是服务器通知客户端 35 36 */ 37 38 //有人下线 协议 39 //[命令(11)| ip(下线的ip)| ...] 40 41 //我这里通知客户端吧 42 foreach (KeyValuePair<string, Socket> item in Common.connSocket) 43 { 44 Thread thread = new Thread(() => 45 { 46 byte[] buffer = Encoding.UTF8.GetBytes(outIp); 47 List<byte> list = buffer.ToList(); 48 list.Insert(0, 11);//添加协议位 49 item.Value.Send(list.ToArray()); 50 }); 51 thread.IsBackground = true; 52 thread.Start(); 53 54 55 //多线程。通知每个在线用户。更新列表 56 //ThreadPool.QueueUserWorkItem(new WaitCallback(new Action<object>((o) => 57 //{ 58 // string result = string.Join(",", Common.connSocket.Keys); 59 // byte[] buffer = Encoding.UTF8.GetBytes(result); 60 // try 61 // { 62 // //客户端关闭,则发送报异常 63 // item.Value.Send(buffer); 64 // } 65 // catch (Exception ex) 66 // { 67 68 // } 69 //}))); 70 //string result = string.Join(",", Common.connSocket.Keys); 71 //byte[] buffer = Encoding.UTF8.GetBytes(result); 72 //item.Value.Send(buffer); 73 74 } 75 }
有没有想过。服务器是怎么知道客户端发送的是文字。文件还是抖动呢?
这里就要提到自定义协议了。双方约定好。比如:
客户端发送0 就代表是文字。 发送1 则代表是文件 2 则代表是抖动。
我这里自定义的协议如下。不能说很好。只能说马马虎虎过得去
1 /*********************协议说明***********************/ 2 //根据协议解码 3 /* 4 * 自定义协议规则 5 * [ 命令(1) | 内容(30) | ip(22)| 响应(....) | ...] 6 命令:0-文件 1-文字 2-震动 7 * 内容: 8 * 文件(长度,文件名字+后缀名) 响应(文件的字节) 9 * 文字(内容) 10 * 震动() 11 * ip(自己的ip和对方的ip) 12 * 13 * [ 命令(1) | ip(22) | 内容(30)| 响应(....) | ...] 14 * 15 * 文件: 16 * [命令(0)| ip(自己的ip和对方的ip)| 内容(文件大小和文件全名)|响应(文件内容) | ...] 17 * 文字: 18 * [命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 19 * 震动 20 * [命令(2)| ip(对方的ip)| ...] 21 * 更新在线人数 22 * [命令(3)| ip(自己的ip和对方的ip)| ...] 23 * 第一次登陆获取在线(好友)人数 24 * [命令(10)| ip(自己的ip和所有的ip)| ...] 25 * 有人下线 26 * [命令(11)| ip(下线的ip)| ...] 27 * 有人上线 28 * [命令(12)| ip(上线的ip)| ...] 29 * 0 30 * 1 31 * 2 32 * 3 33 * 4 34 * 5 35 * 36 */
既然协议有了。那么就可以根据协议发送数据包。服务器和其他客户端就可以根据协议解包。
这里来说下发送文件的协议
从上面可以看到我定义的文件协议:
[命令(0)| ip(自己的ip和对方的ip)| 内容(文件大小和文件全名)|响应(文件内容) | ...]
先贴上发送文件的代码(客户端发送)。然后根据我的思路来分析
客户端发送文件代码
1 /// <summary> 2 /// 发送文件 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 void btnSendFile_Click(object sender, EventArgs e) 7 { 8 //判断是否有选择用户 9 string sendUser = cbList.Text; 10 11 if (string.IsNullOrEmpty(sendUser)) 12 { 13 //提示 14 tbHis.AppendText("请选择要发送的用户...\n"); 15 return; 16 } 17 //判断是否选择了文件 18 else if (string.IsNullOrEmpty(tbFile.Text)) 19 { 20 tbHis.AppendText("请选择文件\n"); 21 tbHis.AppendText("\n"); 22 return; 23 } 24 //开始读取文件 25 using (FileStream fs = new FileStream(tbFile.Text, FileMode.Open, FileAccess.Read)) 26 { 27 //大文件会内存溢出 28 //引发类型为“System.OutOfMemoryException”的异常。 29 //所以大文件只能 续传。像QQ一样在线接收的方式。 30 byte[] buffer = new byte[fs.Length]; 31 //获取实际的字节数,如果有需要的话。 32 int num = fs.Read(buffer, 0, buffer.Length); 33 34 /*协议: 这里50位不知道是否理想?? 35 * 是不是可以修改为:第一位 协议 第二位标记ip的长度 第三位标记内容的长度?? 36 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30)|响应(文件内容) | ...] 37 */ 38 string allIp = string.Format("{0},{1}", cbList.Text, myIp.Text); 39 byte[] sendIp = Encoding.UTF8.GetBytes(allIp); 40 41 List<byte> list = sendIp.ToList(); 42 43 //sendIp 不够50位 44 if (sendIp.Length < 50) 45 { 46 for (int i = 0; i < 50 - sendIp.Length; i++) 47 { 48 list.Add(0); 49 } 50 } 51 list.Insert(0, 0); //添加协议位 52 //添加内容 53 byte[] fileByte = Encoding.UTF8.GetBytes(Common.SafeFileName); 54 list.AddRange(fileByte); 55 //内容是否够30 56 if (fileByte.Length < 30) 57 { 58 for (int i = 0; i < 30 - fileByte.Length; i++) 59 { 60 list.Add(0); 61 } 62 } 63 //添加响应 64 list.AddRange(buffer); 65 66 //开始发送 67 Common.connSocket.Send(list.ToArray()); 68 } 69 }
字节数组必须根据你定义的协议来拼接。这样在服务端才好解包
我这里把ip放到了List<byte>中
List<byte> list = sendIp.ToList();
然后在list前面插入协议位:0 ,因为我定义的协议是0代表发送文件
list.Insert(0, 0); //添加协议位
是否还记得上面提到的。客户端接收文件。打开保存对话框。自动补全了文件名。是怎么做的吗?
因为协议中发文件名发过去了
//添加内容
byte[] fileByte = Encoding.UTF8.GetBytes(Common.SafeFileName);
list.AddRange(fileByte);
还有因为不知道ip的固定长度。所以我这里限制是50位。不够的话就补加空,不知道这样是否划算
//sendIp 不够50位 if (sendIp.Length < 50) { for (int i = 0; i < 50 - sendIp.Length; i++) { list.Add(0); } }
所以的数据根据协议打包后。就可以发送到服务器。在服务器解包。获取目的者的ip。开始转发
Receive方法中你可以找到解包的代码,部分如下:
1 byte[] data = new byte[num]; 2 //复制实际的长度到data字节数组中 3 Array.Copy(Common.ReceiveBuffer, 0, data, 0, num); 4 5 //判断协议位 6 int command = data[0]; 7 8 //获取内容 9 string source = Encoding.UTF8.GetString(data, 1, num - 1); 10 11 //获取接收者的ip 12 string receiveIp = source.Split(',')[0]; 13 14 15 if (command == 1) //说明发送的是文字 16 { 17 /*协议: 18 * //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 19 */ 20 //获取接收者通信连接的socket 21 if (Common.connSocket.ContainsKey(receiveIp)) 22 { 23 Common.connSocket[receiveIp].Send(data); 24 } 25 }
获取协议位:
int command = data[0];
注:
你会发现我这里在服务器解包判断协议毫无意义。因为第一位是命令。而后面接着是ip。这些长度都是固定的。
只有取出来ip发送到指定的客户端。让客户端解包判断协议就是了。没错。因为我这里本来加个功能。如果是文件则把文件缓存起来。所以就判断
了协议的命令。你可以根据自己的需求去做。
发送到了客户端,就理所当然的要判断协议位了。如果是抖动就抖一下。是文件。就提示是否接受。等等
看看客户端解包代码:
记住:下面的Receive方法是客户端中的方法。用力接收服务器发来的消息
1 /// <summary> 2 /// 接收来自服务器的消息 3 /// </summary> 4 /// <param name="result"></param> 5 void Receive(IAsyncResult result) 6 { 7 Socket clientSocket = result.AsyncState as Socket; 8 9 //byte[] b = new byte[1024 * 1024]; 10 try 11 { 12 //获取实际的长度 13 int num = clientSocket.EndReceive(result); 14 if (num > 0) 15 { 16 //MessageBox.Show(num.ToString()); 17 byte[] buffer = new byte[num]; 18 Array.Copy(Common.buffer, 0, buffer, 0, num); //复制数据到data 19 //string ip = Encoding.UTF8.GetString(data); 20 21 22 //以下是客户端=》服务器==》服务器 23 /* 24 * 当if else 超过3个 25 * 26 * 建议用switch case语句 27 */ 28 29 //获取口令 30 int command = buffer[0]; 31 //说明是获取好友 32 if (command == 10) 33 { 34 //协议说明 35 //第一次登陆获取在线(好友)人数 36 //[命令(10)| ip(自己的ip和所有好友的ip)| ...] 37 38 39 //其实用户本地ip也可以这样获取 40 //string cy = clientSocket.LocalEndPoint; 41 42 string allIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 43 string[] temp = allIp.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 44 45 46 //跨线程操作UI 47 this.Invoke(new Action(() => 48 { 49 myIp.Text = temp.Length > 0 ? temp[temp.Length - 1] : ""; 50 51 //排除自己的ip 52 var other = from i in temp where !i.Contains(myIp.Text) select i; 53 54 cbList.Items.Clear();//清空 55 cbList.Items.AddRange(other.ToArray()); //绑定列表 56 57 if (cbList.Items.Count > 0) 58 cbList.SelectedIndex = 0;//默认选中第一个 59 })); 60 61 } 62 else if (command == 11) //说明是有人下线 63 { 64 //协议说明 65 // 有人下线 66 //[命令(11)| ip(下线的ip)| ...] 67 68 //获取下线的ip 69 string outIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 70 71 this.Invoke(new Action(() => 72 { 73 //删除下线的ip 74 cbList.Items.Remove(outIp); 75 if (cbList.Items.Count > 0) 76 cbList.SelectedIndex = 0;//默认选中第一个 77 })); 78 } 79 else if (command == 12) //有人上线了 80 { 81 //协议说明 82 // 有人上线 83 //[命令(12)| ip(上线的ip)| ...] 84 85 //获取上线的ip 86 string onlineIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 87 //添加上线的ip 88 89 this.Invoke(new Action(() => 90 { 91 //添加上线的ip 92 cbList.Items.Add(onlineIp); 93 if (cbList.Items.Count > 0) 94 cbList.SelectedIndex = 0;//默认选中第一个 95 })); 96 } 97 98 //以下是客户端=》服务器==》客户端 99 100 else if (command == 1) 101 { 102 //协议: 103 //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 104 105 //获取ip段 106 string[] sourceIp = Encoding.UTF8.GetString(buffer, 1, 50).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 107 108 //发消息来的ip 109 string fromIp = sourceIp[1]; 110 111 112 113 //获取内容 114 string content = Encoding.UTF8.GetString(buffer, 50 + 1, num - 50 - 1); 115 116 this.Invoke(new Action(() => 117 { 118 //列表框中选择当前的ip 119 cbList.Text = fromIp.ToString(); 120 121 //显示内容 122 tbHis.AppendText(string.Format("时间:{0}\n", DateTime.Now.ToString())); 123 //tbHis.AppendText(string.Format("提示{0}对我说:", fromIp)); //我操。这样怎么就不行 124 tbHis.AppendText(fromIp + "\n"); 125 tbHis.AppendText("对你说:\n"); 126 tbHis.AppendText(content + "\n"); 127 tbHis.AppendText("\n"); 128 })); 129 } 130 else if (command == 0) //发送的文件 131 { 132 /*协议: 这里50位不知道是否理想。 133 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30位)|响应(文件内容) | ...] 134 */ 135 136 //这里有冗余代码 137 138 //获取ip段 139 string[] sourceIp = Encoding.UTF8.GetString(buffer, 1, 50).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 140 141 //发消息来的ip 142 string fromIp = sourceIp[1]; 143 144 this.Invoke(new Action(() => 145 { 146 //列表框中选择当前的ip 147 cbList.Text = fromIp.ToString(); 148 })); 149 150 151 //获取内容 152 string content = Encoding.UTF8.GetString(buffer, 50 + 1, 30); 153 154 //获取响应 155 //string pass = Encoding.UTF8.GetString(buffer, 50 + 30 + 1, num - 50 - 30 - 1); 156 157 //显示 158 //tbHis.AppendText(string.Format("{0}给你发了一个文件:{1}\n", fromIp, content)); 159 tbHis.AppendText(fromIp); 160 tbHis.AppendText("给你发了一个文件"); 161 tbHis.AppendText(content + "\n"); 162 tbHis.AppendText("\n"); 163 164 165 //提示用户是否接收文件 166 if (MessageBox.Show("是否接受文件\n" + content, "接收文件", MessageBoxButtons.YesNo) == DialogResult.Yes) 167 { 168 //开始保存 169 SaveFileDialog sfd = new SaveFileDialog(); 170 sfd.FileName = content; 171 172 //获取文件类型 173 string ex = content.Split('.')[1]; 174 175 //保存文件类型 176 sfd.Filter = "|*." + ex; 177 if (sfd.ShowDialog(this) == DialogResult.OK) 178 { 179 using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create)) 180 { 181 fs.Write(buffer, 50 + 30 + 1, num - 50 - 30 - 1); 182 } 183 } 184 185 } 186 187 } 188 else if (command == 2) //发送抖动 189 { 190 //如果窗口在任务栏。则显示 191 this.Show(); 192 this.WindowState = FormWindowState.Normal; 193 this.Activate(); 194 195 int n = -1; 196 197 for (int i = 0; i < 10; i++) 198 { 199 n = -n; 200 this.Location = new Point(this.Location.X + 10 * n, this.Location.Y + 10 * n); 201 this.TopMost = true;//在所有窗口的顶部 202 System.Threading.Thread.Sleep(50); 203 } 204 205 //抖动完成。结束顶层显示 206 this.TopMost = false; 207 } 208 209 //连接成功,再一次获取服务器发来的消息 210 clientSocket.BeginReceive(Common.buffer, 0, Common.buffer.Length, 0, new AsyncCallback(Receive), clientSocket); 211 212 } 213 } 214 catch (Exception ex) //服务器端口 215 { 216 MessageBox.Show(ex.Message); 217 clientSocket.Shutdown(SocketShutdown.Receive); 218 clientSocket.Close(); 219 } 220 }
都是if else if判断的。着实看着会晕乎乎的。因为是小案列。就没去过多的封装。大家可以根据自己的喜好去封装
上面大部分都是讲的服务端口的代码。相比客户端就是一样的。只是少了一个监听的代码。直接连接服务器。接收和发送消息就ok了
这里就不分析了。稍后直接贴上参考代码
当然socket通信协议方面的知识不单单就我这点皮毛。大家自己可以深入研究。
比如:很多网站右下角都会有这样一个窗口,大家都见过吧。
我想这也是相同的原理。当打开浏览器。ajax异步去连接服务器,
获取消息发送到客户端。你说呢?
因为想做个打飞机的游戏。其实网上也有很多源码,可我认为别人的终究是别人的。只有自己做过那才叫曾经拥有。
可单机版的太没有挑战性,那就实现一个多人在线一起玩的。就涉及到了服务器
所以想用C#写一个服务器,unity3d做客户端,与之通信。所以才有了这篇博文的诞生。
最后把全部源码给大家参考下
服务端源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 using System.Net; 10 using System.Net.Sockets; 11 using System.Threading; 12 13 namespace TopServer 14 { 15 public partial class mainServer : Form 16 { 17 18 /*********************协议说明***********************/ 19 //根据协议解码 20 /* 21 * 自定义协议规则 22 * [ 命令(1) | 内容(30) | ip(22)| 响应(....) | ...] 23 命令:0-文件 1-文字 2-震动 24 * 内容: 25 * 文件(长度,文件名字+后缀名) 响应(文件的字节) 26 * 文字(内容) 27 * 震动() 28 * ip(自己的ip和对方的ip) 29 * 30 * [ 命令(1) | ip(22) | 内容(30)| 响应(....) | ...] 31 * 32 * 文件: 33 * [命令(0)| ip(自己的ip和对方的ip)| 内容(文件大小和文件全名)|响应(文件内容) | ...] 34 * 文字: 35 * [命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 36 * 震动 37 * [命令(2)| ip(对方的ip)| ...] 38 * 更新在线人数 39 * [命令(3)| ip(自己的ip和对方的ip)| ...] 40 * 第一次登陆获取在线(好友)人数 41 * [命令(10)| ip(自己的ip和所有的ip)| ...] 42 * 有人下线 43 * [命令(11)| ip(下线的ip)| ...] 44 * 有人上线 45 * [命令(12)| ip(上线的ip)| ...] 46 * 0 47 * 1 48 * 2 49 * 3 50 * 4 51 * 5 52 * 53 */ 54 55 public mainServer() 56 { 57 InitializeComponent(); 58 Init(); 59 } 60 61 /// <summary> 62 /// 初始化datagridview属性 63 /// </summary> 64 public void Init() 65 { 66 67 #region datagridview一些属性设置 68 dgvList.AllowUserToAddRows = false; 69 dgvList.AllowUserToDeleteRows = false; 70 dgvList.AllowUserToResizeColumns = false; 71 dgvList.AllowUserToResizeRows = false; 72 dgvList.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; 73 dgvList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; 74 dgvList.MultiSelect = false; 75 dgvList.ReadOnly = true; 76 dgvList.RowHeadersVisible = false; 77 dgvList.BackgroundColor = Color.White; 78 dgvList.ScrollBars = ScrollBars.Vertical; 79 dgvList.SelectionMode = DataGridViewSelectionMode.FullRowSelect; 80 #endregion 81 82 //窗体加载事件 83 //this.Load += new EventHandler(mainServer_Load); 84 85 //启动服务器按钮 86 this.btnStart.Click += new EventHandler(btnStart_Click); 87 88 //关闭服务器按钮 89 this.btnStop.Click += new EventHandler(btnStop_Click); 90 } 91 92 /// <summary> 93 /// 关闭服务器 94 /// </summary> 95 /// <param name="sender"></param> 96 /// <param name="e"></param> 97 void btnStop_Click(object sender, EventArgs e) 98 { 99 if (Common.ListenSocket != null) 100 { 101 Common.ListenSocket.Close(); 102 Common.ListenSocket = null; 103 btnStart.Enabled = true; 104 //底部提示消息 105 tssMsg.Text = "服务器已经关闭"; 106 } 107 } 108 109 /// <summary> 110 /// 启动服务器 111 /// </summary> 112 /// <param name="sender"></param> 113 /// <param name="e"></param> 114 void btnStart_Click(object sender, EventArgs e) 115 { 116 StartServer(); 117 } 118 119 /// <summary> 120 /// 窗体加载事件 121 /// </summary> 122 /// <param name="sender"></param> 123 /// <param name="e"></param> 124 void mainServer_Load(object sender, EventArgs e) 125 { 126 //启动时间 127 startTime.Text = DateTime.Now.ToString(); 128 129 //启动服务器 130 StartServer(); 131 } 132 133 /// <summary> 134 /// 打开服务器 135 /// </summary> 136 void StartServer() 137 { 138 try 139 { 140 string _ip = tbIp.Text; 141 int _point = int.Parse(tbPoint.Text); 142 143 //创建监听客户端请求的socket 144 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 145 //监听用的ip和端口 146 IPAddress address = IPAddress.Parse(_ip); 147 IPEndPoint point = new IPEndPoint(address, _point); 148 149 //绑定 150 socket.Bind(point); 151 socket.Listen(10); 152 153 154 //异步 开始监听 155 socket.BeginAccept(new AsyncCallback(Listen), socket); 156 157 158 //禁用当前按钮 159 btnStart.Enabled = false; 160 161 //启动时间 162 startTime.Text = DateTime.Now.ToString(); 163 164 //底部提示消息 165 tssMsg.Text = "服务器已经启动"; 166 } 167 catch (Exception ex) 168 { 169 MessageBox.Show(ex.Message); 170 } 171 172 } 173 174 /// <summary> 175 /// 开始监听 176 /// </summary> 177 /// <param name="result"></param> 178 void Listen(IAsyncResult result) 179 { 180 181 try 182 { 183 //获取监听的socket 184 Socket clientSocket = result.AsyncState as Socket; 185 //与服务器通信的socket 186 Socket connSocket = clientSocket.EndAccept(result); 187 188 string ip = connSocket.RemoteEndPoint.ToString(); 189 //连接成功。保存信息 190 if (!Common.connSocket.ContainsKey(ip)) 191 Common.connSocket.Add(ip, connSocket); 192 193 //连接成功,更新服务器信息 194 changeList(connSocket); 195 196 197 //等待新的客户端连接 ,相当于循环调用 198 clientSocket.BeginAccept(new AsyncCallback(Listen), clientSocket); 199 200 byte[] buffer = new byte[1024 * 1024]; 201 202 203 //接收来自客户端信息 ,相当于循环调用 204 connSocket.BeginReceive(Common.ReceiveBuffer, 0, Common.ReceiveBuffer.Length, 0, new AsyncCallback(Receive), connSocket); 205 206 //用户第一次登陆。获取所有在线用户 如果有好友功能。则获取他的好友 207 SendesClient(connSocket); 208 } 209 catch (Exception ex) 210 { 211 212 //MessageBox.Show(ex.Message); 213 } 214 215 216 } 217 /// <summary> 218 /// 接收来自客户端信息 219 /// </summary> 220 /// <param name="result"></param> 221 void Receive(IAsyncResult result) 222 { 223 224 //与客户端通信的socket 225 Socket clientSocket = result.AsyncState as Socket; 226 227 try 228 { 229 //获取实际的长度值 230 int num = clientSocket.EndReceive(result); 231 if (num > 0) 232 { 233 byte[] data = new byte[num]; 234 //复制实际的长度到data字节数组中 235 Array.Copy(Common.ReceiveBuffer, 0, data, 0, num); 236 237 //判断协议位 238 int command = data[0]; 239 240 //获取内容 241 string source = Encoding.UTF8.GetString(data, 1, num - 1); 242 243 //获取接收者的ip 244 string receiveIp = source.Split(',')[0]; 245 246 247 if (command == 1) //说明发送的是文字 248 { 249 /*协议: 250 * //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 251 */ 252 //获取接收者通信连接的socket 253 if (Common.connSocket.ContainsKey(receiveIp)) 254 { 255 Common.connSocket[receiveIp].Send(data); 256 } 257 } 258 else if (command == 0) //说明是发送的文件 259 { 260 /*协议: 这里50位不知道是否理想。 261 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30位)|响应(文件内容) | ...] 262 */ 263 264 //获取接收者通信连接的socket 265 if (Common.connSocket.ContainsKey(receiveIp)) 266 { 267 Common.connSocket[receiveIp].Send(data); 268 } 269 } 270 else if (command == 2)//抖动一下 271 { 272 //协议 273 //震动 274 //[命令(2)| 对方的ip和自己的ip 50位| ...] 275 276 //获取接收者通信连接的socket 277 if (Common.connSocket.ContainsKey(receiveIp)) 278 { 279 Common.connSocket[receiveIp].Send(data); 280 } 281 } 282 283 //string msg = Encoding.UTF8.GetString(data); 284 //MessageBox.Show(msg); 285 286 287 //接收其他信息 288 clientSocket.BeginReceive(Common.ReceiveBuffer, 0, Common.ReceiveBuffer.Length, 0, new AsyncCallback(Receive), clientSocket); 289 290 } 291 else //客户端断开 292 { 293 clientOff(clientSocket); 294 } 295 } 296 catch (Exception ex) 297 { 298 clientOff(clientSocket); 299 } 300 301 } 302 303 /// <summary> 304 /// 客户端关闭 305 /// </summary> 306 void clientOff(Socket clientSocket) 307 { 308 //从集合删除下线的ip 309 string outIp = clientSocket.RemoteEndPoint.ToString(); 310 if (Common.connSocket.ContainsKey(outIp)) 311 Common.connSocket.Remove(outIp); 312 313 //更新服务器在线人数 314 changOnlineCount(false); 315 316 this.Invoke(new Action(() => 317 { 318 //更新列表 319 //删除退出的ip 320 for (int i = 0; i < dgvList.Rows.Count; i++) 321 { 322 if (dgvList.Rows[i].Tag.ToString() == outIp) 323 { 324 dgvList.Rows.RemoveAt(i); 325 break; 326 } 327 } 328 })); 329 330 clientSocket.Shutdown(SocketShutdown.Receive); 331 clientSocket.Close(); 332 333 //通知所有在线用户。有人下线了。需要更新列表,如果是qq是通知我的好友。不知道是不是这样 334 /*这里有点疑问: 335 * 是客户端定时到服务器获取在线用户? 336 * 还是服务器通知客户端 337 338 */ 339 340 //有人下线 协议 341 //[命令(11)| ip(下线的ip)| ...] 342 343 //我这里通知客户端吧 344 foreach (KeyValuePair<string, Socket> item in Common.connSocket) 345 { 346 Thread thread = new Thread(() => 347 { 348 byte[] buffer = Encoding.UTF8.GetBytes(outIp); 349 List<byte> list = buffer.ToList(); 350 list.Insert(0, 11);//添加协议位 351 item.Value.Send(list.ToArray()); 352 }); 353 thread.IsBackground = true; 354 thread.Start(); 355 356 357 //多线程。通知每个在线用户。更新列表 358 //ThreadPool.QueueUserWorkItem(new WaitCallback(new Action<object>((o) => 359 //{ 360 // string result = string.Join(",", Common.connSocket.Keys); 361 // byte[] buffer = Encoding.UTF8.GetBytes(result); 362 // try 363 // { 364 // //客户端关闭,则发送报异常 365 // item.Value.Send(buffer); 366 // } 367 // catch (Exception ex) 368 // { 369 370 // } 371 //}))); 372 //string result = string.Join(",", Common.connSocket.Keys); 373 //byte[] buffer = Encoding.UTF8.GetBytes(result); 374 //item.Value.Send(buffer); 375 376 } 377 } 378 /// <summary> 379 /// 把上线的人发送到客户端 380 /// </summary> 381 /// <param name="connSocket">当前连接的客户端</param> 382 void SendesClient(Socket connSocket) 383 { 384 //自定义协议:[命令 2位] 385 /* 386 * 第一位:10代表是首次登陆获取所有好友,把自己的ip放最后一位 387 * 好像这里默认已经是最后一位了?? 388 */ 389 string key = connSocket.RemoteEndPoint.ToString(); 390 //把自己的ip删除 391 if (Common.connSocket.ContainsKey(key)) 392 Common.connSocket.Remove(key); 393 394 //把自己的key添加到最后一位 395 if (!Common.connSocket.ContainsKey(key)) 396 Common.connSocket.Add(key, connSocket); 397 398 //发送到客户端 399 byte[] clientByte = Encoding.UTF8.GetBytes(string.Join(",", Common.connSocket.Keys)); 400 401 402 //List<byte> bbb = new List<byte>(); 403 //bbb.Add(1); 404 //bbb.AddRange(clientByte); 405 406 List<byte> li = clientByte.ToList(); 407 li.Insert(0, 10);//第一位插入10 代表是获取好友 408 409 //把当前在线ip发送给自己 410 connSocket.Send(li.ToArray()); 411 412 //告诉其他在线的用户。我上线啦,求勾搭 413 //var online = from onn in Common.connSocket 414 // where !onn.Key.Contains(connSocket.RemoteEndPoint.ToString()) //筛选,不包含自己的ip 415 // select onn; 416 417 //if (online.Count() <= 0) return; //当前没有上线的 418 419 foreach (KeyValuePair<string, Socket> item in Common.connSocket) 420 { 421 //不需要给自己发送。因为当自己上线的时候。就已经获取了在线的ip 422 if (item.Key == connSocket.RemoteEndPoint.ToString()) continue; 423 //多线程通知在线用户。 424 Thread thread = new Thread(() => 425 { 426 byte[] buffer = Encoding.UTF8.GetBytes(connSocket.RemoteEndPoint.ToString()); 427 List<byte> list = buffer.ToList(); 428 //有人上线 429 //[命令(12)| ip(上线的ip)| ...] 430 list.Insert(0, 12);//说明有人上线 431 item.Value.Send(list.ToArray()); 432 }); 433 thread.IsBackground = true; 434 thread.Start(); 435 } 436 437 //判断当前用户是否还处于连接状态 438 //if (Common.connSocket.ContainsKey(connSocket.RemoteEndPoint.ToString())) 439 //connSocket.Send(buffer); 440 441 //有人下线。就通知所有在线的人数 442 //如果是qq我想应该是通知我的好友。不是知道是不是 443 444 /* 445 * 如果在线有 A B C 446 * 那么A下线 447 * 是不是要通知B C 448 * 还是在B C 定时访问服务器来获取在线人数呢? 449 * 待解决 450 */ 451 } 452 453 /// <summary> 454 /// 更新列表 455 /// </summary> 456 /// <param name="socket"></param> 457 void changeList(Socket socket) 458 { 459 //获取客户端信息 ip和端口号 460 string ip = socket.RemoteEndPoint.ToString(); 461 //客户端登陆时间 462 string time = DateTime.Now.ToString(); 463 464 //跨线程操作ui 465 this.Invoke(new Action(() => 466 { 467 //新增一行 468 dgvList.Rows.Add(); 469 470 //获取当前dgvList的行 471 int rows = dgvList.Rows.Count; 472 473 //赋值 474 dgvList.Rows[rows - 1].Cells[0].Value = ip; 475 dgvList.Rows[rows - 1].Cells[1].Value = time; 476 477 //把ip当作当前行的tag标记一下,为了删除行的时候可以找到该行 478 dgvList.Rows[rows - 1].Tag = ip; 479 480 //更新在线人数 481 //lbCount.Text = int.Parse(lbCount.Text) + 1 + "";//后面加空字符串。转为字符串 482 //或者 483 //lbCount.Text = (int.Parse(lbCount.Text) + 1).ToString(); 484 485 //foreach (DataGridViewRow item in dgvList.Rows) 486 //{ 487 // if(item.Tag==ip)item. 488 489 //} 490 491 492 //dgvList.DataSource = Common.connSocket; 493 494 //更新在线人数 495 changOnlineCount(true); 496 })); 497 498 } 499 500 /// <summary> 501 /// 更新在线人数 502 /// </summary> 503 /// <param name="tag">true=>+ false=>-</param> 504 void changOnlineCount(bool tag) 505 { 506 int num = 0; 507 if (tag) num = int.Parse(lbCount.Text) + 1; 508 else num = int.Parse(lbCount.Text) - 1; 509 510 this.Invoke(new Action(() => 511 { 512 //更新在线人数 513 lbCount.Text = num.ToString(); 514 if (num == 0) Common.connSocket.Clear(); 515 516 })); 517 } 518 } 519 520 /// <summary> 521 /// 公共类 522 /// </summary> 523 public class Common 524 { 525 /// <summary> 526 /// 保存服务器来的消息 527 /// </summary> 528 public static byte[] ReceiveBuffer = new byte[1024 * 1024]; 529 530 /// <summary> 531 /// 监听用的socket 532 /// </summary> 533 public static Socket ListenSocket; 534 535 /// <summary> 536 /// 保存所有负责通信用是socket 537 /// </summary> 538 public static Dictionary<string, Socket> connSocket = new Dictionary<string, Socket>(); 539 } 540 }
客户端源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Windows.Forms; 9 using System.Net.Sockets; 10 using System.Net; 11 using System.Threading; 12 using System.IO; 13 14 namespace TopClient 15 { 16 public partial class UserList : Form 17 { 18 public UserList() 19 { 20 InitializeComponent(); 21 Init(); 22 } 23 public void Init() 24 { 25 //窗体加载 26 this.Load += new EventHandler(UserList_Load); 27 28 //发送文字 29 this.btnSender.Click += new EventHandler(btnSender_Click); 30 31 //选择文件 32 this.btnChangeFile.Click += new EventHandler(btnChangeFile_Click); 33 34 //发送文件 35 this.btnSendFile.Click += new EventHandler(btnSendFile_Click); 36 37 //抖动对方 38 this.btnDd.Click += new EventHandler(btnDd_Click); 39 40 //创建time 41 /* 42 * 用力监听服务器是否开启 43 */ 44 //Common.time = new System.Windows.Forms.Timer(); 45 //Common.time.Interval = 1000; //如果服务器未开启。则一秒访问一次 46 //Common.time.Tick += new EventHandler(time_Tick); 47 } 48 /// <summary> 49 /// 发送抖动 50 /// </summary> 51 /// <param name="sender"></param> 52 /// <param name="e"></param> 53 void btnDd_Click(object sender, EventArgs e) 54 { 55 //判断是否有选择用户 56 string sendUser = cbList.Text; 57 58 if (string.IsNullOrEmpty(sendUser)) 59 { 60 //提示 61 tbHis.AppendText("请选择要抖动的用户...\n"); 62 return; 63 } 64 //协议 65 //震动 66 //[命令(2)| 对方的ip和自己的ip 50位| ...] 67 string allIp = string.Format("{0},{1}", cbList.Text, myIp.Text); 68 69 byte[] sendIp = Encoding.UTF8.GetBytes(allIp); 70 71 List<byte> list = sendIp.ToList(); 72 list.Insert(0, 2);//添加协议位 73 74 //sendIp 不够50位 75 if (sendIp.Length < 50) 76 { 77 for (int i = 0; i < 50 - sendIp.Length; i++) 78 { 79 list.Add(0); 80 } 81 } 82 //开始发送 83 Common.connSocket.Send(list.ToArray()); 84 } 85 86 /// <summary> 87 /// 发送文件 88 /// </summary> 89 /// <param name="sender"></param> 90 /// <param name="e"></param> 91 void btnSendFile_Click(object sender, EventArgs e) 92 { 93 //判断是否有选择用户 94 string sendUser = cbList.Text; 95 96 if (string.IsNullOrEmpty(sendUser)) 97 { 98 //提示 99 tbHis.AppendText("请选择要发送的用户...\n"); 100 return; 101 } 102 //判断是否选择了文件 103 else if (string.IsNullOrEmpty(tbFile.Text)) 104 { 105 tbHis.AppendText("请选择文件\n"); 106 tbHis.AppendText("\n"); 107 return; 108 } 109 //开始读取文件 110 using (FileStream fs = new FileStream(tbFile.Text, FileMode.Open, FileAccess.Read)) 111 { 112 //大文件会内存溢出 113 //引发类型为“System.OutOfMemoryException”的异常。 114 //所以大文件只能 续传。像QQ一样在线接收的方式。 115 byte[] buffer = new byte[fs.Length]; 116 //获取实际的字节数,如果有需要的话。 117 int num = fs.Read(buffer, 0, buffer.Length); 118 119 /*协议: 这里50位不知道是否理想?? 120 * 是不是可以修改为:第一位 协议 第二位标记ip的长度 第三位标记内容的长度?? 121 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30)|响应(文件内容) | ...] 122 */ 123 string allIp = string.Format("{0},{1}", cbList.Text, myIp.Text); 124 byte[] sendIp = Encoding.UTF8.GetBytes(allIp); 125 126 List<byte> list = sendIp.ToList(); 127 128 //sendIp 不够50位 129 if (sendIp.Length < 50) 130 { 131 for (int i = 0; i < 50 - sendIp.Length; i++) 132 { 133 list.Add(0); 134 } 135 } 136 list.Insert(0, 0); //添加协议位 137 //添加内容 138 byte[] fileByte = Encoding.UTF8.GetBytes(Common.SafeFileName); 139 list.AddRange(fileByte); 140 //内容是否够30 141 if (fileByte.Length < 30) 142 { 143 for (int i = 0; i < 30 - fileByte.Length; i++) 144 { 145 list.Add(0); 146 } 147 } 148 //添加响应 149 list.AddRange(buffer); 150 151 //开始发送 152 Common.connSocket.Send(list.ToArray()); 153 } 154 } 155 156 /// <summary> 157 /// 选择文件 158 /// </summary> 159 /// <param name="sender"></param> 160 /// <param name="e"></param> 161 void btnChangeFile_Click(object sender, EventArgs e) 162 { 163 OpenFileDialog ofd = new OpenFileDialog(); 164 if (ofd.ShowDialog() == DialogResult.OK) 165 { 166 tbFile.Text = ofd.FileName; 167 //保存文件名和扩展名 168 Common.SafeFileName = ofd.SafeFileName; 169 } 170 } 171 /// <summary> 172 /// 173 /// </summary> 174 /// <param name="sender"></param> 175 /// <param name="e"></param> 176 void time_Tick(object sender, EventArgs e) 177 { 178 connectServer(); 179 } 180 181 /// <summary> 182 /// 发送 183 /// </summary> 184 /// <param name="sender"></param> 185 /// <param name="e"></param> 186 void btnSender_Click(object sender, EventArgs e) 187 { 188 //判断是否有选择用户 189 string sendUser = cbList.Text; 190 //获取聊天的内容 191 string content = tbContent.Text; 192 193 if (string.IsNullOrEmpty(sendUser)) 194 { 195 //提示 196 tbHis.AppendText("请选择要发送的用户...\n"); 197 return; 198 } 199 else if (string.IsNullOrEmpty(content)) 200 { 201 //提示 202 tbHis.AppendText("你不打算输入点什么咯...\n"); 203 return; 204 } 205 206 try 207 { 208 //文字: ip设置最大值为 50 位 209 //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 210 //这里把对方的ip放前面是为了在服务端好获取 211 //把自己的ip和对方的ip转为byte 212 213 //如果是独立电脑应该可以这样获取 214 //Common.connSocket.LocalEndPoint 215 //如果此处不发送自己的ip。那么也可以在服务端获取 216 217 string allIp = string.Format("{0},{1}", cbList.Text, myIp.Text); 218 219 220 byte[] sendIp = Encoding.UTF8.GetBytes(allIp); 221 byte[] buffer = Encoding.UTF8.GetBytes(content); 222 223 List<byte> list = sendIp.ToList(); 224 list.Insert(0, 1);//添加协议位 225 226 //sendIp 不够50位 227 if (sendIp.Length < 50) 228 { 229 for (int i = 0; i < 50 - sendIp.Length; i++) 230 { 231 list.Add(0); 232 } 233 } 234 235 //把内容添加到末尾 236 list.AddRange(buffer); 237 238 //开始发送 239 Common.connSocket.Send(list.ToArray()); 240 tbContent.Clear(); 241 242 //把发送的内容显示在上面 243 tbHis.AppendText(string.Format("时间:{0}\n", DateTime.Now.ToString())); 244 tbHis.AppendText(string.Format("我对{0}说:\n", cbList.Text)); 245 tbHis.AppendText(content + "\n"); 246 tbHis.AppendText("\n"); 247 //清空输入框 248 tbContent.Clear(); 249 } 250 catch (Exception ex) 251 { 252 MessageBox.Show(ex.Message); 253 } 254 255 } 256 //保存服务器来的byte 257 byte[] buffer = new byte[1024 * 1024]; 258 //保存ip 259 public static string ip; 260 void UserList_Load(object sender, EventArgs e) 261 { 262 connectServer(); 263 } 264 265 void connectServer() 266 { 267 string ip = "192.168.1.2"; 268 int _point = 8000; 269 270 try 271 { 272 Common.connSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 273 IPAddress address = IPAddress.Parse(ip); 274 IPEndPoint point = new IPEndPoint(address, _point); 275 Common.connSocket.Connect(point); 276 277 //连接成功,获取服务器发来的消息 278 Common.connSocket.BeginReceive(Common.buffer, 0, Common.buffer.Length, 0, new AsyncCallback(Receive), Common.connSocket); 279 280 //Thread t = new Thread(get); 281 //t.IsBackground = true; 282 //t.Start(Common.connSocket); 283 284 } 285 catch (Exception ex) 286 { 287 MessageBox.Show(ex.Message); 288 } 289 } 290 291 void get(object o) 292 { 293 while (true) 294 { 295 Socket clientSocket = o as Socket; 296 297 byte[] buffer = new byte[1024 * 1024]; 298 try 299 { 300 //获取实际的长度 301 int num = clientSocket.Receive(buffer); //服务器关闭会报错 302 if (num > 0) 303 { 304 //获取口令 305 int command = buffer[0]; 306 //说明是获取好友 307 if (command == 10) 308 { 309 //协议说明 310 //第一次登陆获取在线(好友)人数 311 //[命令(10)| ip(自己的ip和所有好友的ip)| ...] 312 313 314 //其实用户本地ip也可以这样获取 315 //string cy = clientSocket.LocalEndPoint; 316 317 string allIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 318 string[] temp = allIp.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 319 320 321 //跨线程操作UI 322 this.Invoke(new Action(() => 323 { 324 myIp.Text = temp.Length > 0 ? temp[temp.Length - 1] : ""; 325 326 //排除自己的ip 327 var other = from i in temp where !i.Contains(myIp.Text) select i; 328 329 cbList.Items.Clear();//清空 330 cbList.Items.AddRange(other.ToArray()); //绑定列表 331 })); 332 333 } 334 else if (command == 11) //说明是有人下线 335 { 336 //协议说明 337 // 有人下线 338 //[命令(11)| ip(下线的ip)| ...] 339 340 //获取下线的ip 341 string outIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 342 343 this.Invoke(new Action(() => 344 { 345 //删除下线的ip 346 cbList.Items.Remove(outIp); 347 })); 348 } 349 else if (command == 12) //有人上线了 350 { 351 //协议说明 352 // 有人上线 353 //[命令(12)| ip(上线的ip)| ...] 354 355 //获取上线的ip 356 string onlineIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 357 //添加上线的ip 358 359 this.Invoke(new Action(() => 360 { 361 //添加上线的ip 362 cbList.Items.Add(onlineIp); 363 })); 364 365 } 366 367 this.Invoke(new Action(() => 368 { 369 if (cbList.Items.Count > 0) 370 cbList.SelectedIndex = 0;//默认选中第一个 371 372 })); 373 } 374 } 375 catch (Exception ex) 376 { 377 MessageBox.Show(ex.Message); 378 clientSocket.Shutdown(SocketShutdown.Receive); 379 clientSocket.Close(); 380 break; 381 } 382 383 384 //string ip = Encoding.UTF8.GetString(buffer, 0, num); 385 ////MessageBox.Show(ip); 386 387 ////第一个是自己的ip 388 //string[] userIp = ip.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 389 390 //try 391 //{ 392 393 // //cbList.Items.Clear(); 394 // //MessageBox.Show(cbList.Items.Count.ToString()); 395 396 // //var m0 = from m in userIp 397 // // where !m.Contains(userIp[0].ToString()) 398 // // select m; 399 // //cbList.Items.AddRange(userIp.ToArray()); 400 401 // //this.Invoke(new Action(() => 402 // //{ 403 // // cbList.Items.Clear(); 404 // // //MessageBox.Show(cbList.Items.Count.ToString()); 405 406 // // cbList.Items.AddRange(userIp.ToArray()); 407 408 409 // //})); 410 //} 411 //catch (Exception ex) 412 //{ 413 // MessageBox.Show(ex.Message); 414 //} 415 416 417 //MessageBox.Show(string.Join(",",userIp)); 418 419 //cbList.DataSource = userIp.ToList(); 420 421 422 423 } 424 } 425 426 /// <summary> 427 /// 接收来自服务器的消息 428 /// </summary> 429 /// <param name="result"></param> 430 void Receive(IAsyncResult result) 431 { 432 Socket clientSocket = result.AsyncState as Socket; 433 434 //byte[] b = new byte[1024 * 1024]; 435 try 436 { 437 //获取实际的长度 438 int num = clientSocket.EndReceive(result); 439 if (num > 0) 440 { 441 //MessageBox.Show(num.ToString()); 442 byte[] buffer = new byte[num]; 443 Array.Copy(Common.buffer, 0, buffer, 0, num); //复制数据到data 444 //string ip = Encoding.UTF8.GetString(data); 445 446 447 //以下是客户端=》服务器==》服务器 448 /* 449 * 当if else 超过3个 450 * 451 * 建议用switch case语句 452 */ 453 454 //获取口令 455 int command = buffer[0]; 456 //说明是获取好友 457 if (command == 10) 458 { 459 //协议说明 460 //第一次登陆获取在线(好友)人数 461 //[命令(10)| ip(自己的ip和所有好友的ip)| ...] 462 463 464 //其实用户本地ip也可以这样获取 465 //string cy = clientSocket.LocalEndPoint; 466 467 string allIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 468 string[] temp = allIp.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 469 470 471 //跨线程操作UI 472 this.Invoke(new Action(() => 473 { 474 myIp.Text = temp.Length > 0 ? temp[temp.Length - 1] : ""; 475 476 //排除自己的ip 477 var other = from i in temp where !i.Contains(myIp.Text) select i; 478 479 cbList.Items.Clear();//清空 480 cbList.Items.AddRange(other.ToArray()); //绑定列表 481 482 if (cbList.Items.Count > 0) 483 cbList.SelectedIndex = 0;//默认选中第一个 484 })); 485 486 } 487 else if (command == 11) //说明是有人下线 488 { 489 //协议说明 490 // 有人下线 491 //[命令(11)| ip(下线的ip)| ...] 492 493 //获取下线的ip 494 string outIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 495 496 this.Invoke(new Action(() => 497 { 498 //删除下线的ip 499 cbList.Items.Remove(outIp); 500 if (cbList.Items.Count > 0) 501 cbList.SelectedIndex = 0;//默认选中第一个 502 })); 503 } 504 else if (command == 12) //有人上线了 505 { 506 //协议说明 507 // 有人上线 508 //[命令(12)| ip(上线的ip)| ...] 509 510 //获取上线的ip 511 string onlineIp = Encoding.UTF8.GetString(buffer, 1, num - 1); 512 //添加上线的ip 513 514 this.Invoke(new Action(() => 515 { 516 //添加上线的ip 517 cbList.Items.Add(onlineIp); 518 if (cbList.Items.Count > 0) 519 cbList.SelectedIndex = 0;//默认选中第一个 520 })); 521 } 522 523 //以下是客户端=》服务器==》客户端 524 525 else if (command == 1) 526 { 527 //协议: 528 //[命令(1)|对方的ip和自己的ip 50位)| 内容(文字) | ...] 529 530 //获取ip段 531 string[] sourceIp = Encoding.UTF8.GetString(buffer, 1, 50).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 532 533 //发消息来的ip 534 string fromIp = sourceIp[1]; 535 536 537 538 //获取内容 539 string content = Encoding.UTF8.GetString(buffer, 50 + 1, num - 50 - 1); 540 541 this.Invoke(new Action(() => 542 { 543 //列表框中选择当前的ip 544 cbList.Text = fromIp.ToString(); 545 546 //显示内容 547 tbHis.AppendText(string.Format("时间:{0}\n", DateTime.Now.ToString())); 548 //tbHis.AppendText(string.Format("提示{0}对我说:", fromIp)); //我操。这样怎么就不行 549 tbHis.AppendText(fromIp + "\n"); 550 tbHis.AppendText("对你说:\n"); 551 tbHis.AppendText(content + "\n"); 552 tbHis.AppendText("\n"); 553 })); 554 } 555 else if (command == 0) //发送的文件 556 { 557 /*协议: 这里50位不知道是否理想。 558 * [命令(0)| ip(对方的ip和自己的ip 50位)| 内容(文件大小和文件全名 30位)|响应(文件内容) | ...] 559 */ 560 561 //这里有冗余代码 562 563 //获取ip段 564 string[] sourceIp = Encoding.UTF8.GetString(buffer, 1, 50).Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); 565 566 //发消息来的ip 567 string fromIp = sourceIp[1]; 568 569 this.Invoke(new Action(() => 570 { 571 //列表框中选择当前的ip 572 cbList.Text = fromIp.ToString(); 573 })); 574 575 576 //获取内容 577 string content = Encoding.UTF8.GetString(buffer, 50 + 1, 30); 578 579 //获取响应 580 //string pass = Encoding.UTF8.GetString(buffer, 50 + 30 + 1, num - 50 - 30 - 1); 581 582 //显示 583 //tbHis.AppendText(string.Format("{0}给你发了一个文件:{1}\n", fromIp, content)); 584 tbHis.AppendText(fromIp); 585 tbHis.AppendText("给你发了一个文件"); 586 tbHis.AppendText(content + "\n"); 587 tbHis.AppendText("\n"); 588 589 590 //提示用户是否接收文件 591 if (MessageBox.Show("是否接受文件\n" + content, "接收文件", MessageBoxButtons.YesNo) == DialogResult.Yes) 592 { 593 //开始保存 594 SaveFileDialog sfd = new SaveFileDialog(); 595 sfd.FileName = content; 596 597 //获取文件类型 598 string ex = content.Split('.')[1]; 599 600 //保存文件类型 601 sfd.Filter = "|*." + ex; 602 if (sfd.ShowDialog(this) == DialogResult.OK) 603 { 604 using (FileStream fs = new FileStream(sfd.FileName, FileMode.Create)) 605 { 606 fs.Write(buffer, 50 + 30 + 1, num - 50 - 30 - 1); 607 } 608 } 609 610 } 611 612 } 613 else if (command == 2) //发送抖动 614 { 615 //如果窗口在任务栏。则显示 616 this.Show(); 617 this.WindowState = FormWindowState.Normal; 618 this.Activate(); 619 620 int n = -1; 621 622 for (int i = 0; i < 10; i++) 623 { 624 n = -n; 625 this.Location = new Point(this.Location.X + 10 * n, this.Location.Y + 10 * n); 626 this.TopMost = true;//在所有窗口的顶部 627 System.Threading.Thread.Sleep(50); 628 } 629 630 //抖动完成。结束顶层显示 631 this.TopMost = false; 632 } 633 634 //连接成功,再一次获取服务器发来的消息 635 clientSocket.BeginReceive(Common.buffer, 0, Common.buffer.Length, 0, new AsyncCallback(Receive), clientSocket); 636 637 } 638 } 639 catch (Exception ex) //服务器端口 640 { 641 MessageBox.Show(ex.Message); 642 clientSocket.Shutdown(SocketShutdown.Receive); 643 clientSocket.Close(); 644 } 645 } 646 } 647 }
客户端的一个公共类 Common.cs
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Net; 6 using System.Net.Sockets; 7 8 namespace TopClient 9 { 10 /**************************************** 11 * 与服务器通信的类 12 ****************************************/ 13 public class Common 14 { 15 /// <summary> 16 /// 与服务器通信的socket 17 /// </summary> 18 public static Socket connSocket; 19 20 /// <summary> 21 /// 保存服务器来的byte 22 /// </summary> 23 public static byte[] buffer = new byte[1024 * 1024]; 24 25 /// <summary> 26 /// 保存当登陆成功后。从服务器获取的所有用ip 27 /// </summary> 28 public static string ip; 29 30 /// <summary> 31 /// time计时器 32 /// </summary> 33 public static System.Windows.Forms.Timer time; 34 35 /// <summary> 36 /// 当前是否连接到服务器 37 /// </summary> 38 public static bool isConnect = false; 39 40 /// <summary> 41 /// 保存文件的文件名和扩展名 xxx.png 42 /// </summary> 43 public static string SafeFileName; 44 } 45 }