在公司花了两天的时间来研究Socket编程,感觉也还是学到了一点点皮毛,不过也还是有一点点的小成就。
配合Winform窗体界面,实现了简单的窗体间互相通信的小项目(可以互相发消息,服务器可以给客户端发送文件和震动弹出)。
现在把这两天学习到的知识点记录下来分享。。。
首先要声明:本篇博客并不会详细的讲解Socket底层的实现逻辑,只是讲解他是怎么运用的,是怎么实现窗体间通信的。
Socket的简单原理
Socket的两个概念:端口和协议。
端口
我们是通过服务端去访问应用程序的,但是在同一个服务端中的应用程序,他们的IP地址都是一样的,所以单单通过IP地址去访问对应的应用程序是不可能的,所以,得再结合端口去实现具体的访问。
每个应用程序就会有一个端口,我们就可以根据IP地址和端口号去实现访问了。(每个应用程序的端口号都是唯一的)
通过端口和IP地址客户端就可以准确无误的去访问服务器里面的应用程序了。
在访问的过程中,又会涉及到协议的问题!
协议
Socket有两个协议:TCP 和 UDP;
TCP
其中我们要知道,TCP协议是网络上比较安全稳定的协议,一般不会发送数据丢失。使用TCP协议建立网络连接,需要经过“三次握手”才建立连接。如下图:
只有客户端和服务器完成了这三次握手的过程,服务器才会和客户端进行数据的传输。(只要是少了一次都不会进行互相通信)
TCP建立网络连接的优缺点:
- 优点:安全,稳定,防止数据丢失。
- 缺点:效率低,经过三次握手的过程,需要耗费一定的时间。
UDP
UDP协议是与TCP协议相反的。
UDP协议不管服务器有没有空,就是一直给服务器发送消息,直到发完为止。他不管服务器有没有接收到,他只是要完成任务就行。
如果服务器很忙的话,并没有时间去处理客户端发过来的消息,那么就会造成数据丢失。
UDP建立网络连接的优缺点:
- 优点:快速,效率高。
- 缺点:不稳定,容易造成数据丢失。
两个协议各有的优缺点,也不好说哪个比较好。
像UDP协议一般用于视频的传输等。
好了TCP/UDP协议了解到这里就好了!
Socket
需要包含命名空间:
using System.Net;
using System.Net.Sockets;
一个服务器至少有两个Socket用于通信,一个用来监听,一个用来连接客户端进行通信。
我们先看一张图:
这张图描述的就是客户端和服务其进行互相通信的过程。
下面的讲解也是根据这张图来说明。
- Socket
他是一个类,用来定义对象进行通信。
首先创建一个用于监听的Socket(监听有没有客户端连接服务器)
如代码:
// 一个负责监听的Socket ************************************************************************
// ip地址的类型(ipV4/ipv6) 选择以流的方式 流对应的是Tcp协议
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
参数一:填写IP地址的类型(ipv4:InterNetwork; ipv6:InterNetworkV6)
参数二:填写TCP/UDP连接对应的方式
补充知识点:TCP是以流的方式(Stream);UDP是以数据报的方式(Dgram)。
参数三:填写TCP/UDP协议
这里就用了TCP的传输方式。
然后创建ip地址和端口号对象:
// 创建ip地址和端口号对象
IPAddress iPAddress = IPAddress.Any; //IPAddress.Parse(this.txtSever.Text);
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, Convert.ToInt32(this.txtPort.Text));
Any是自动获取连接进行的客户端的IP地址。
端口号是获取我们textBox控件里面的。
-
Bind
绑定监听端口// 让负责监听的Socket绑定IP地址和端口号 socketWatch.Bind(iPEndPoint);
到了这里,就完成了监听的工作。
-
Listen
设置监听队列
在同一个时间段允许最大的连接个数// 设置监听队列(比如在同一个时间段允许最大的连接个数) socketWatch.Listen(10);
比如在一秒内最多允许10个客户端连接服务器,从第11个客户端往后都得排队等待连接。
-
Accept
该函数用于等待客户端与服务器进行连接,并返回一个新的Socket,用于与客户端进行通信。// 负责监听的Socket来接受客户端的连接(创建跟客户端通信的Socket) Socket socket = socketWatch.Accept(); // Accept() : 等待客户端连接
利用死循环可以一直与多个客户端进行连接
为了不卡死主线程,可以使用多线程对其进行操作!// 该线程的作用是:服务器不停的监听,等待客户端连接,并且创建与之通信用的Socket Thread th = new Thread(Listen); th.IsBackground = true; th.Start(socketWatch);
private void Listen(Object o) { Socket socketWatch = o as Socket; // 等待客户端连接,并且创建与之通信用的Socket while (true) { // 负责监听的Socket来接受客户端的连接(创建跟客户端通信的Socket) Socket socket= socketWatch.Accept(); // Accept() : 等待客户端连接 } }
-
Receive
获取客户端发过来的消息(单位:byte)// 客户端连接成功后,服务器接收客户端发来的消息 byte[] buffer = new byte[1024 * 1024 * 2]; // b:实际接收到的有效字节数 int b = socket.Receive(buffer); // 调用Receive函数返回客户端发来的消息(单位:字节)
可以根据b来判断客户端发过来的消息的字节个数。
-
Send
服务器给客户端发送消息仅支持字节发送数组发送
byte[] newByte = Encoding.UTF8.GetBytes("我是服务器"); socket.Send(newByte);
好了,服务器的流程基本上就这样了。
客户端与服务端雷同,就不演示了。
练手小项目
可能讲的不是很好,但是没关系,我把我自己写好的代码上传到此给需要的朋友玩一下。(可以实现两个窗口间互相通信)
界面如下:
服务器:
客户端:
代码如下:
服务器
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _Socket {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e) {
Control.CheckForIllegalCrossThreadCalls = false;
}
private void btnStart_Click(object sender, EventArgs e) {
try {
// 一个负责监听的Socket ************************************************************************
// ip地址的类型(ipV4/ipv6) 选择以流的方式 流对应的是Tcp协议
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 创建ip地址和端口号对象
IPAddress iPAddress = IPAddress.Any; //IPAddress.Parse(this.txtSever.Text);
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, Convert.ToInt32(this.txtPort.Text));
// 让负责监听的Socket绑定IP地址和端口号
socketWatch.Bind(iPEndPoint);
ShowMsg("监听成功!");
// 设置监听队列(比如在同一个时间段允许最大的连接个数)
socketWatch.Listen(10);
// 该线程的作用是:服务器不停的监听,等待客户端连接,并且创建与之通信用的Socket
Thread th = new Thread(Listen);
th.IsBackground = true;
th.Start(socketWatch);
}
catch (Exception) { }
}
// 存储连接服务器的客户端的IP地址/端口号和负责通信的socket
Dictionary<string, Socket> dictionary = new Dictionary<string, Socket>();
Socket socket; // 负责通信
private void Listen(Object o) {
Socket socketWatch = o as Socket;
// 等待客户端连接,并且创建与之通信用的Socket
while (true) {
try {
// 负责监听的Socket来接受客户端的连接(创建跟客户端通信的Socket)
socket = socketWatch.Accept(); // Accept() : 等待客户端连接
// 将连接成功后的客户端IP地址和端口号显示出来
ShowMsg(socket.RemoteEndPoint.ToString() + "连接成功\r\n");
// 将远程连接的客户端的IP地址和socket存入集合中
dictionary.Add(socket.RemoteEndPoint.ToString(), socket);
// 将远程连接的客户端的IP地址和端口号存储到下拉框中
this.cobUsers.Items.Add(socket.RemoteEndPoint.ToString());
// 该线程的作用是:使服务器不停的接收客户端发过来的消息
Thread th = new Thread(Recive);
th.IsBackground = true;
th.Start(socket);
}
catch (Exception) { }
}
}
// 服务器不停的接收客户端发过来的消息
private void Recive(Object o) {
Socket socket = o as Socket;
while (true) {
try {
// 客户端连接成功后,服务器接收客户端发来的消息
byte[] buffer = new byte[1024 * 1024 * 2];
// b:实际接收到的有效字节数
int b = socket.Receive(buffer); // 调用Receive函数返回客户端发来的消息(单位:字节)
// 用户点击了叉×
if (b == 0) {
break;
}
// 将其转为人能看得懂的字符串类型
string str = Encoding.UTF8.GetString(buffer, 0, b);
ShowMsg(socket.RemoteEndPoint.ToString() + "(客户端):" + str); // 显示出来
}
catch (Exception) { }
}
}
private void ShowMsg(string str) {
this.txtLog.AppendText(str + "\r\n");
}
private void btnSend_Click(object sender, EventArgs e) {
try {
// 获取文本的值并转换为byte类型数组
byte[] by = Encoding.UTF8.GetBytes(this.txtMsg.Text.Trim());
this.txtMsg.Text = "";
// 定义一个泛型集合,将文本表示符0 和 byte数组存储入集合中
List<byte> list = new List<byte>();
list.Add(0);
list.AddRange(by);
// 将泛型集合转换为数组
byte[] newByte = list.ToArray();
// 服务器给客户端发送消息
//socket.Send(by);
string ip = this.cobUsers.SelectedItem.ToString(); // 获得下拉框中选中项的IP地址和端口号
dictionary[ip].Send(newByte); // 将其传入Dictionary中获得跟客户端通信的socket
}
catch (Exception) { }
}
// 选择需要发送的文件
private void btnSelect_Click(object sender, EventArgs e) {
try {
OpenFileDialog ofd = new OpenFileDialog();
// 设置初始路径
ofd.InitialDirectory = @"C:\Users\yangg\Desktop";
ofd.Title = "请选择你需要发送的文件";
// 设置文件筛选
ofd.Filter = "所有文件|*.*";
if (ofd.ShowDialog() == DialogResult.OK) {
this.txtPath.Text = ofd.FileName;
}
}
catch (Exception) { }
}
private void btnSentFile_Click(object sender, EventArgs e) {
try {
// 获取文件路径
string path = this.txtPath.Text;
// 使用文件流以读的方式打开文件
using (FileStream fileRead = new FileStream(path, FileMode.Open, FileAccess.Read)) {
byte[] buffer = new byte[1024 * 1024 * 10];
// 将文件中的数据读取进字节数组中,返回读取到的个数
int r = fileRead.Read(buffer, 0, buffer.Length);
List<byte> list = new List<byte>();
list.Add(1);
list.AddRange(buffer);
byte[] newBuffer = list.ToArray();
// 发送
dictionary[this.cobUsers.SelectedItem.ToString()].Send(newBuffer, 0, r + 1, SocketFlags.None);
}
}
catch (Exception) { }
}
private void btnZD_Click(object sender, EventArgs e) {
byte[] buffer = new byte[1];
buffer[0] = 2;
dictionary[this.cobUsers.SelectedItem.ToString()].Send(buffer);
}
}
}
客户端
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace _Client {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
Socket socket;
private void btnStart_Click(object sender, EventArgs e) {
try {
// 创建一个发送消息的Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 建立IP地址和端口号的对象
IPAddress iPAddress = IPAddress.Parse(this.txtSever.Text);
IPEndPoint iPEndPoint = new IPEndPoint(iPAddress, Convert.ToInt32(this.txtPort.Text));
// 客户端服务器建立连接
socket.Connect(iPEndPoint);
Shows("连接成功!");
// 建立线程,无限的接收服务器发过来的消息
Thread th = new Thread(Recive);
th.IsBackground = true;
th.Start();
}
catch (Exception) { }
}
private void Shows(string str) {
this.txtLog.AppendText(str + "\r\n");
}
// 客户端给服务器发送消息
private void benSend_Click(object sender, EventArgs e) {
try {
// 获取待发送的文本,并转换为byte字节数组
byte[] by = Encoding.UTF8.GetBytes(this.txtMsg.Text.Trim());
this.txtMsg.Text = "";
// 客户端给服务器发送消息
socket.Send(by);
}
catch (Exception) { }
}
// 不停的接收服务器发过来的消息
private void Recive() {
while (true) {
try {
byte[] by = new byte[1024 * 1024 * 10];
// 获取服务器发过来的消息,返回接收到的个数
int r = socket.Receive(by);
if (r == 0) {
break;
}
// 表示发送文字消息
if (by[0] == 0) {
string str = Encoding.UTF8.GetString(by, 1, r - 1);
Shows(socket.RemoteEndPoint.ToString() + "(服务器):" + str);
} else if (by[0] == 1) { // 表示发送文件
SaveFileDialog sfd = new SaveFileDialog();
sfd.InitialDirectory = @"C:\Users\yangg\Desktop";
sfd.Title = "请选择报错位置";
sfd.Filter = "所有文件|*.*";
if (sfd.ShowDialog(this) == DialogResult.OK) {
string path = sfd.FileName;
using (FileStream fileWrite = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write)) {
fileWrite.Write(by, 1, r - 1);
}
MessageBox.Show("保存成功");
}
} else if (by[0] == 2) { // 表示发送震动
ZD();
}
}
catch (Exception) { }
}
}
// 震动
private void ZD() {
int x = this.Location.X;
int y = this.Location.Y;
for (int i = 0; i < 300; i++) {
this.Location = new Point(x - 50, y);
this.Location = new Point(x, y + 50);
this.Location = new Point(x + 50, y);
this.Location = new Point(x, y - 50);
}
this.Location = new Point(x, y);
}
private void Form1_Load(object sender, EventArgs e) {
Control.CheckForIllegalCrossThreadCalls = false;
}
}
}
优快云:
https://download.youkuaiyun.com/download/cpp_learner/12761444
百度连接:
链接:https://pan.baidu.com/s/16TE3Q51NhxMXNv2sp9gfcQ
提取码:fc4n
总结:
也就基本上是这样了,其他的也就不会了,希望对大家有帮助吧!