Socket编程:两个窗口通信
本文章代码来自b站视频:【.Net零基础入门 (老赵主讲)-哔哩哔哩】 https://b23.tv/YI5VWaj
原视频发布者为传智播客,本人根据自己的学习进度对代码做了少许优化
一、网络编程前置知识
1.1 什么是网络编程
-
网络编程从大的方面说就是对信息的发送到接收,中间传输为物理线路的作用。
网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理,代码、开发工具、数据库、服务器架设和网页设计这5部分你都要接触。
1.2 Socket简介
-
用视频的话讲解就是宿管大妈
如图男生要找一个女生,首先要知道宿舍楼位置(服务器ip地址),但是知道ip地址并不能直接找到要找的女生,还需要知道女生的名字(应用的端口号),这两样东西都是被宿管大妈(Socket)管理的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
宿管大妈主要是根据男生提供的女生姓名找到女生,负责的是监听。
通信的Socket是应用自身携带的,可以理解男生和女生的通信是用的自己的嘴说话和耳朵进行信息交流,在程序中我们要为客户端和服务器分别绑定一个Socket负责通信
1.3 协议
- 协议的本质就是约定,语言本身就有约定的存在,比如数字“2”代表数量为二的含义,数字“1”代表数量为一的含义。我们更改一下,让“2”这个字符代表数量一,让“1”代表数量二,如果大家都认可这个约定,我们以后数数就是“213456789…”,对于计算机来说,并不能直接识别字符本身的含义,我们通常在数据的指定位置上加标志位来解决这个问题。比如“0”代表文本信息,“1”代表文件数据,“2”代表动作信息。而网络协议TCP/IP要比我们在本程序中规定的要复杂的多,但是基本原理是一致的。想要深入学习TCP/IP,请参照《计算机网络》这门课程。
二、开始项目
2.1 程序流程图
-
原版流程图是简化的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
-
自己绘制的流程图
2.2 新建项目
-
新建winform(.NET Framework)项目
本人使用的是Visual Studio2022版本,不同版本的Visual Studio操作有细微的区别,没有相关依赖的需要安装。安装方式:工具->获取工具和功能->visual studio installer->选择.NET桌面开发->点击安装。其他版本的请自行百度安装方法或者更换版本。
第一个项目是server端的编写
-
server端页面布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2.3 第一步:初始化服务器
- 点击监听按钮,开始监听
/// <summary> /// 开始监听按钮点击事件,开始监听: /// 1.创建监听Socket对象 /// 2.绑定服务器ip和端口号 /// 3.开启监听 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void startListenBtn_Click(object sender, EventArgs e) { try { //点击开始监听,服务器创建负责监听IP地址与端口号的Socket Socket socketListener=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ip=IPAddress.Any; //创建端口号对象,IPAddress.Any表示服务器接收任意ip地址的访问 IPEndPoint endPoint = new IPEndPoint(ip, Convert.ToInt32(portBox.Text)); socketListener.Bind(endPoint); ShowMsg("监听成功"); //设置时间点内最大访问数 socketListener.Listen(10); //由于监听是个死循环,创建监听函数,需要开启新线程调用它,避免主线程卡死 Thread thred=new Thread(Listen); thred.IsBackground = true; thred.Start(socketListener); } catch { } } /// <summary> /// 监听函数 /// 1.接收监听Socket /// 2.创建死循环,表示监听一直处于开启状态 /// 3.通过Accept函数获取服务器端的负责通信的Socket /// 注:注释的代码是后序功能需要的代码,现在注释是进度需要 /// </summary> /// <param name="o"></param> void Listen(object o)//被线程所执行的函数所携带的参数必须为object类型 { Socket socketListener = o as Socket; // 等待客户端的连接,并且创建与之通信的socket while (true) { Socket socketCom = socketListener.Accept(); /*socketDics.Add(socketCom.RemoteEndPoint.ToString(), socketCom);*/ //将ip地址和端口号存入下拉框 serverSelectComboBox.Items.Add(socketCom.RemoteEndPoint.ToString()); ShowMsg(socketCom.RemoteEndPoint.ToString() + ":" + "连接成功"); /*Thread thred = new Thread(showReceivedMsg); thred.IsBackground = true; thred.Start(socketCom);*/ } } /// <summary> /// log数据显示 /// </summary> /// <param name="msg"></param> void ShowMsg(string msg) { logBox.AppendText(msg+"\r\n"); }
2.4 第二步:初始化客户端
-
客户端布局
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
-
连接服务器代码
/// <summary> /// 连接按钮点击事件,连接到服务器: /// 1.创建通信的的Socket /// 2.从输入框获取ip地址和port /// 3.通过Connect函数连接到服务器 /// 注:注释的代码是后序功能需要的代码,现在注释是进度需要 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> Socket socketSend;//全局变量,方便后面使用 private void connectBtn_Click(object sender, EventArgs e) { try { //创建负责通信的Socket socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ip = IPAddress.Parse(ipAddressBox.Text); EndPoint serverEndPoint = new IPEndPoint(ip, Convert.ToInt32(portBox.Text)); //连接远程服务器的应用端口号,connect返回的始终为空 socketSend.Connect(serverEndPoint); ShowMsg("成功连接服务器:" + serverEndPoint); /*Thread thred = new Thread(showReceivedMsgFromServer); thred.IsBackground = true; thred.Start(socketSend);*/ } catch { ShowMsg("not connected!"); } } void ShowMsg(string msg) { logBox.AppendText(msg+"\r\n"); }
-
程序运行效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
2.5 第三步:客户端向服务器发送信息
-
服务器端代码
void Listen(object o) { Socket socketListener = o as Socket; // 等待客户端的连接,并且创建与之通信的socket while (true) { Socket socketCom = socketListener.Accept(); /*socketDics.Add(socketCom.RemoteEndPoint.ToString(), socketCom);*/ //将ip地址和端口号存入下拉框 serverSelectComboBox.Items.Add(socketCom.RemoteEndPoint.ToString()); ShowMsg(socketCom.RemoteEndPoint.ToString() + ":" + "连接成功"); //打开注释,服务器接收消息也是死循环,所以需要开启新线程调用 Thread thred = new Thread(showReceivedMsg); thred.IsBackground = true; thred.Start(socketCom); } } /// <summary> /// 接收数据 /// 1.接收通信Socket /// 2.开启死循环表示服务器接收处于开启状态 /// 3.开始接收数据,接收方法涉及到IO流知识,不熟悉的同学可以查阅官方文档 /// 文档地址:https://learn.microsoft.com/zh-cn/dotnet/csharp/ /// 4.消息框(logBox)显示信息 /// </summary> /// <param name="o"></param> void showReceivedMsg(object o) { Socket socketCom=o as Socket; while (true) { try { byte[] buffer = new byte[1024 * 1024 * 2]; //实际接收到的有效字节数,接收到数据的同时会给数组赋值 int availableBytes = socketCom.Receive(buffer); //解码 if (availableBytes == 0){ break; } string receivedMessage = Encoding.UTF8.GetString(buffer, 0, availableBytes); ShowMsg(socketCom.RemoteEndPoint.ToString() + ":" + receivedMessage); } catch { } } }
-
客户端代码
/// <summary> /// 发送按钮点击事件,客户端给服务器发送信息 /// 1.获取文本信息 /// 2.把信息编码(字节数组) /// 3.发送数据 /// 4.清空消息发送框 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void sendMsgBtn_Click(object sender, EventArgs e) { //获取数据 string msg = msgBox.Text.Trim(); byte[] data = Encoding.UTF8.GetBytes(msg); //发送数据 socketSend.Send(data); msgBox.Clear(); }
-
程序运行效果
2.5 第四步:服务器向指定的服务器客户端发送信息
-
客户端代码
private void connectBtn_Click(object sender, EventArgs e) { try { //创建负责通信的Socket socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress ip = IPAddress.Parse(ipAddressBox.Text); EndPoint serverEndPoint = new IPEndPoint(ip, Convert.ToInt32(portBox.Text)); //连接远程服务器的应用端口号,connect返回的始终为空 socketSend.Connect(serverEndPoint); ShowMsg("成功连接服务器:" + serverEndPoint); //打开注释 Thread thred = new Thread(showReceivedMsgFromServer); thred.IsBackground = true; thred.Start(socketSend); } catch { ShowMsg("not connected!"); } } /// <summary> /// 信息接收方法 /// 1.创建缓冲池 /// 2.判断标志位,判断标志位是为了后面区分接收的信息类型,方便扩展 /// 3.解码 /// 4.显示消息 /// </summary> /// <param name="o"></param> void showReceivedMsgFromServer(object o) { Socket socketCom=o as Socket; while (true) { try { byte[] buffer = new byte[1024 * 1024 * 5]; //实际接收到的有效字节数,同时会给buffer数组赋值 int availableBytes = socketCom.Receive(buffer); byte flagBit = buffer[0]; //解码 if (availableBytes == 0) { break; } switch (flagBit) { case 0: ShowMsg("标志位是" + flagBit + "您接收的是文本消息"); string receivedMessage = Encoding.UTF8.GetString(buffer, 1, availableBytes-1); ShowMsg(socketCom.RemoteEndPoint.ToString() + ":" + receivedMessage); break; case 1: break; case 2: break; default: break; } } catch{break;} } }
-
服务器的代码
//存储连接到服务器的socket Dictionary<string, Socket> socketDics = new Dictionary<string, Socket>(); void Listen(object o)//被线程所执行的函数所携带的参数必须为object类型 { Socket socketListener = o as Socket; // 等待客户端的连接,并且创建与之通信的socket while (true) { Socket socketCom = socketListener.Accept(); //打开注释,将数据存入字典 socketDics.Add(socketCom.RemoteEndPoint.ToString(), socketCom); //将ip地址和端口号存入下拉框 serverSelectComboBox.Items.Add(socketCom.RemoteEndPoint.ToString()); ShowMsg(socketCom.RemoteEndPoint.ToString() + ":" + "连接成功"); //打开注释 Thread thred = new Thread(showReceivedMsg); thred.IsBackground = true; thred.Start(socketCom); } } /// <summary> /// 发送消息按钮点击事件,向客户端发送数据: /// 1.获取数据 /// 2.添加标志位 /// 3.添加信息位 /// 4.根据下拉框的选择获取ip和端口号 /// 5.根据ip和端口号从字典中获取通信的Socket /// 6.发送消息 /// 7.清空消息发送框 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void sendMessageBox_Click(object sender, EventArgs e) { try { string msg = messageBox.Text; List<byte> dataList = new List<byte>(); byte[] data = Encoding.UTF8.GetBytes(msg); //标志位 dataList.Add(Constant.STRING_FLAG); //信息位 dataList.AddRange(data); //获取通信的socket string serverEndPoint = serverSelectComboBox.SelectedItem.ToString(); Socket socketCom = socketDics[serverEndPoint]; //发送信息 if (socketCom != null) socketCom.Send(dataList.ToArray()); messageBox.Clear(); }catch { } }
//常量池,便于维护 class Constant { public const byte STRING_FLAG = 0; public const byte FILE_FLAG = 1; public const byte ACTION_FLAG = 2; }
-
程序运行效果
2.5 第五步:完善剩余功能:文件,窗口抖动
-
服务器代码
/// <summary> /// 选择按钮点击事件,选择要发送的文件 /// 1.创建打开文件对话框对象 /// 2.设置目录 /// 3.设置属性 /// 4.打开对话框 /// 5.选择文件,把文件名显示到文件框里面 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void selectFileBtn_Click(object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); ofd.InitialDirectory = @"E:\old Downloads\迅雷下载"; ofd.Title = "选择文件"; ofd.Filter = "所有文件|*.*"; ofd.ShowDialog(); fileNameBox.Text = ofd.FileName; } /// <summary> /// 发送文件按钮点击事件,点击后向客户端发送文件 /// 1.从文件文本框从获取文件目录 /// 2.创建文件流 /// 3.创建数据缓冲区 /// 4.读取文件 /// 5.创建List /// 6.List设置标志位 /// 7.List存放信息数组 /// 8.从字典获取通信Socket /// 9.发送文件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void sendFileBtn_Click(object sender, EventArgs e) { try { string filePath = fileNameBox.Text; if (filePath == null) { ShowMsg("未选择文件"); } else { //using 语句定义一个范围,在此范围的末尾将释放对象。 using (FileStream fileRead = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { List<byte> fileData = new List<byte>(); byte[] data = new byte[1024 * 1024 * 5]; //将数据存入缓冲区,并返回读取的长度,读取的同时会向data数组中传入数据 int availableLength = fileRead.Read(data, 0, data.Length); fileData.Add(Constant.FILE_FLAG); fileData.AddRange(data); //发送文件 Socket socketCom = socketDics[serverSelectComboBox.SelectedItem.ToString()]; if (socketCom == null) ShowMsg("未选择ip地址"); else socketCom.Send(fileData.ToArray(), 0, availableLength + 1, SocketFlags.None); } } } catch { } } /// <summary> /// 发送窗口抖动 /// 窗口抖动只需要发送标志位,不需要额外信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void action_Click(object sender, EventArgs e) { try { byte[] actionData = { Constant.ACTION_FLAG }; Socket socketCom = socketDics[serverSelectComboBox.SelectedItem.ToString()]; if (socketCom == null) ShowMsg("未选择ip地址"); else socketCom.Send(actionData); }catch { } }
-
客户端代码
void showReceivedMsgFromServer(object o) { Socket socketCom=o as Socket; while (true) { try { byte[] buffer = new byte[1024 * 1024 * 5]; //实际接收到的有效字节数 int availableBytes = socketCom.Receive(buffer); byte flagBit = buffer[0]; //解码 if (availableBytes == 0) { break; } switch (flagBit) { case 0: /* ...接收文本信息的代码 */ break; case 1: ShowMsg("标志位是" + flagBit + "您接收的是文件类型的数据"); //弹出保存窗口 SaveFileDialog saveFileDialog = new SaveFileDialog(); string path = InitSaveFileDialog(saveFileDialog); using (FileStream fileWrite=new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write)) { fileWrite.Write(buffer, 1, availableBytes - 1); } ShowMsg("文件保存成功"); break; case 2: ShakingForm(); break; default: break; } } catch{break;} } } private string InitSaveFileDialog(SaveFileDialog saveFileDialog) { saveFileDialog.InitialDirectory = @"E:\old Downloads\迅雷下载"; saveFileDialog.Filter = "所有文件|*.*"; saveFileDialog.Title = "保存文件"; saveFileDialog.ShowDialog(this); return saveFileDialog.FileName; } private void ShakingForm() { for(int i = 0; i < 500; i++) { this.Location = new Point(200, 200); this.Location = new Point(210, 210); } }
三、注意事项
3.1 本机ip地址
-
每个人电脑的ip地址可以自己更改,所以地址会发生变动,查看本机ip方法:
打开cmd命令行窗口,输入命令
ipconfig /all
3.2 线程冲突
-
线程冲突问题需要在form的加载事件中解决,取消线程检查。单击窗体进入Load事件
private void SocketClientForm_Load(object sender, EventArgs e) { Control.CheckForIllegalCrossThreadCalls = false; }
private void SocketCommunication_Load(object sender, EventArgs e) { //代码用来防止多线程错误,如果出现多线程错误,可以打开这个注释 //Control.CheckForIllegalCrossThreadCalls = false; }
3.3 传输文件大小问题
- 本程序不能传输过大的文件,这需要更高级的知识