目录
Socket
使用Socket实现TCP客户端和服务器的过程
使用Socket搭建TCP服务器
线程优化
向客户端发送消息
连接的断开
客户端主动断开
服务端主动断开
服务器完整的程序
使用Socket编写客户端程序连接TCP服务器
Socket
Socket是一种网络通信协议,它允许不同计算机上的应用程序通过网络进行数据传输和通信。 Socket协议基于TCP/IP协议族,可以使用TCP或UDP协议进行数据传输。在Socket协议中,通信涉及两个主要部分:服务端和客户端。服务端提供服务并等待客户端的连接请求,而客户端发起连接请求并与服务端建立连接后进行数据传输。Socket协议使用IP地址和端口号来唯一标识网络中的进程或应用程序,通过这种方式,应用程序可以建立与服务端的连接并进行数据的收发
在C#中,一般使用Socket类来完成Tcp、Udp协议的连接和操作,我们使用一个简单的例子学习如何创建一个TCP服务器,以及如何连接TCP服务器进行通讯
使用Socket实现TCP客户端和服务器的过程
使用Socket搭建TCP服务器
[端口号的范围是从0到65535]
我们循序渐进的搭建一个TCP服务器,这是对以下代码块一些参数的简单描述
* AddressFamily:指当前 Socket 的寻址方案
* InterNetwork->IPV4
* InterNetworkV6->IPV6
* SocketType:指定当前套接字实例的类型
* Stream(TCP):支持可靠、双向、基于连接的字节流,而无需复制数据,不保留边界。一个Socket这种类型的通信与单个对等方并在可以开始通信之前需要远程主机的连接
* Dgram(UDP):支持数据报,即为固定(通常很小)的最大长度的无连接的、不可靠的消息。消息可能会丢失或重复,并且可能不按顺序抵达。一个Socket类型的SocketType.Dgram不需要任何连接之前发送和接收数据,并且可以与多个对等方通信。
* ProtocolType:使用哪种协议类型(TCP/UDP)
// 1.创建 socket 链接
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 2、绑定socket的ip地址和端口号
// IPAddress.Parse():IP地址将字符串转换为IPAddress实例。
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("192.168.107.81"),3030);
socket.Bind(endPoint);
// 3.将 socket 置为监听状态
// backlog:挂起连接队列的最大长度。
socket.Listen(100);
// 4.接收来自客户端的连接请求(创建一个新Socket为新创建的连接)
// 该代码将会卡住进程,直到有客户端连接到当前Socket服务时向下执行
Socket socketClient = socket.Accept();
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socketClient.RemoteEndPoint}已经连接\r\n");
}));
// 5. Receive:从 socket 中读取字符(接收该连接发送的数据)
// 该代码将会卡住进程,直到接收到客户端数据时向下执行
byte[] buffer = new byte[1024]; // 大小按情况而定
int length = socketClient.Receive(buffer); // 返回本次接收数据的字节数
string value = Encoding.Default.GetString(buffer, 0, length);
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"接收到{socketClient.RemoteEndPoint}发送的消息:{value}\r\n");
}));
线程优化
在实际运用中可能面临着一下问题
1. 卡线程,导致页面处于假死状态
2. 只能接收一次数据,第二次接收时服务器读不到
3. 只能一个客户端连接
我们可以使用分线程、循环解决,代码优化后如下
private void button1_Click(object sender, EventArgs e)
{
// 1.创建 socket 链接
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 2、绑定socket的ip地址和端口号
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 3030);
socket.Bind(endPoint);
// 3.将 socket 置为监听状态
socket.Listen(100);
// 4、开启连接
startSocket(socket);
}
/// <summary>
/// 在分线程中循环监听来自客户端的连接请求
/// </summary>
/// <param name="socket">Socket连接对象</param>
private void startSocket(Socket socket)
{
// 在分线程中循环监听来自客户端的连接
Task.Run(() =>
{
while (true)
{
// 4.接收来自客户端的连接请求(创建一个新Socket为新创建的连接)
Socket socketClient = socket.Accept();
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socketClient.RemoteEndPoint}已经连接\r\n");
}));
startRecive(socketClient);
}
});
}
/// <summary>
/// 在分线程中循环监听来自客户端的数据
/// </summary>
/// <param name="socket">客户端连接Socket对象</param>
private void startRecive(Socket socket)
{
Task.Run(() =>
{
while (true)
{
// 5.Receive:从 socket 中读取字符(接收该连接发送的数据)
// 该代码将会卡住进程,直到接收到客户端数据时向下执行
byte[] buffer = new byte[1024]; // 大小按情况而定
int length = socket.Receive(buffer); // 返回本次接收数据的字节数
string value = Encoding.Default.GetString(buffer, 0, length);
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"接收到{socket.RemoteEndPoint}发送的消息:{value}\r\n");
}));
}
});
}
向客户端发送消息
通过调用客户端Socket对象的Send方法可以发送数据到客户端,首先,我们应该将所有的客户端连接都进行保存。修改`startSocket`代码如下
使用异步监听连接的客户端并且保存:
Dictionary<string, Socket> socketList = new Dictionary<string, Socket>();
private void startSocket(Socket socket)
{
// 在分线程中循环监听来自客户端的连接
Task.Run(() =>
{
while (true)
{
// 接收来自客户端的连接请求(创建一个新Socket为新创建的连接)
Socket socketClient = socket.Accept();
// 将客户端连接Socket存储到字典中,以IP和端口为key
socketList.Add(socketClient.RemoteEndPoint.ToString(), socketClient);
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socketClient.RemoteEndPoint}已经连接\r\n");
}));
startRecive(socketClient);
}
});
}
使用循环将需要发送的消息逐个发送给客户端
// 发送信息给客户端
private void sendBtn_Click(object sender, EventArgs e)
{
byte[] bytes = Encoding.Default.GetBytes(msgTextbox.Text);
// 循环所有的客户端发送数据(也可以根据IP选择给谁发送)
foreach (var item in socketList)
{
item.Value.Send(bytes);
}
}
连接的断开
连接的断开分为客户端主动断开、服务器主动断开两种
客户端主动断开
一般情况下,当服务端接收到一个空的数据包时,表示客户端要断开连接。
如果客户端被强制终止,会来不及发送一个空的数据包,我们的代码将会抛出错误,也应断开连接。
我们修改`startRecive`代码如下
private void startRecive(Socket socket)
{
Task.Run(() =>
{
while (true)
{
try
{
byte[] buffer = new byte[1024];
int length = socket.Receive(buffer);
if (length == 0)
{
// 从字典中移除当前连接
socketList.Remove(socket.RemoteEndPoint.ToString());
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socket.RemoteEndPoint}连接已断开\r\n");
}));
// 退出循环,停止侦听数据
break;
}
else
{
string value = Encoding.Default.GetString(buffer, 0, length);
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"接收到{socket.RemoteEndPoint}发送的消息:{value}\r\n");
}));
}
}
catch
{
socketList.Remove(socket.RemoteEndPoint.ToString());
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socket.RemoteEndPoint}连接已断开\r\n");
}));
// 退出循环,停止侦听数据
break;
}
}
});
}
服务端主动断开
服务端断开连接要将所有客户端的连接都断开,关闭代码如下,需修改`startSocket`方法中的代码
// 关闭按钮点击
private void closeBtn_Click(object sender, EventArgs e)
{
// 控制按钮状态
closeBtn.Enabled = false;
openBtn.Enabled = true;
// 停止所有客户端连接
foreach (var item in socketList)
{
Socket socket = item.Value;
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
socketList.Clear(); // 清空字典
// 关闭客户端socket
socket.Close();
socket = null;
}
private void startSocket(Socket socket)
{
// 在分线程中循环监听来自客户端的连接
Task.Run(() =>
{
while (true)
{
try
{
Socket socketClient = socket.Accept();
socketList.Add(socketClient.RemoteEndPoint.ToString(), socketClient);
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText($"{socketClient.RemoteEndPoint}已经连接\r\n");
}));
startRecive(socketClient);
}
catch (Exception ex)
{ // 当socket被关闭后,Accept会抛出错误,抛出错误时结束循环,关闭线程
socket.Close();
break;
}
}
});
}
服务器完整的程序
Socket socket;
Dictionary<string, Socket> socketList = new Dictionary<string, Socket>();
private void button1_Click(object sender, EventArgs e)
{
// 按钮状态
openBtn.Enabled = false;
closeBtn.Enabled = true;
// 1.创建 socket 链接
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 2、绑定socket的ip地址和端口号
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, int.Parse(textBox2.Text));
socket.Bind(endPoint);
// 3.将 socket 置为监听状态
socket.Listen(100);
// 4、开启连接
startSocket(socket);
}
/// <summary>
/// 在分线程中循环监听来自客户端的连接请求
/// </summary>
/// <param name="socket">Socket连接对象</param>
private void startSocket(Socket socket)
{
// 在分线程中循环监听来自客户端的连接
Task.Run(() =>
{
while (true)
{
try
{
// 接收来自客户端的连接请求(创建一个新Socket为新创建的连接)
Socket socketClient = socket.Accept();
// 将客户端连接Socket存储到字典中,以IP和端口为key
socketList.Add(socketClient.RemoteEndPoint.ToString(), socketClient);
addRichTextBox($"{socketClient.RemoteEndPoint}已经连接\r\n");
startRecive(socketClient);
}
catch (Exception ex)
{
socket.Close();
break;
}
}
});
}
/// <summary>
/// 在分线程中循环监听来自客户端的数据
/// </summary>
/// <param name="socket">客户端连接Socket对象</param>
private void startRecive(Socket socket)
{
string ip = socket.RemoteEndPoint.ToString();
Task.Run(() =>
{
while (true)
{
try
{
// 5.Receive:从 socket 中读取字符(接收该连接发送的数据)
// 该代码将会卡住进程,直到接收到客户端数据时向下执行
byte[] buffer = new byte[1024]; // 大小按情况而定
int length = socket.Receive(buffer); // 返回本次接收数据的字节数
if (length == 0)
{
throw new Exception($"客户端{ip}断开连接"); // 抛出异常进行终止
}
else
{
string value = Encoding.Default.GetString(buffer, 0, length);
addRichTextBox($"接收到{ip}发送的消息:{value}\r\n");
}
}
catch
{
// 从字典中移除当前连接
socketList.Remove(ip);
addRichTextBox($"{ip}连接已断开\r\n");
break;
}
}
});
}
// 发送信息给客户端
private void sendBtn_Click(object sender, EventArgs e)
{
byte[] bytes = Encoding.Default.GetBytes(msgTextbox.Text);
// 循环所有的客户端发送数据(也可以根据IP选择给谁发送)
foreach (var item in socketList)
{
item.Value.Send(bytes);
}
}
// 关闭按钮点击
private void closeBtn_Click(object sender, EventArgs e)
{
// 控制按钮状态
closeBtn.Enabled = false;
openBtn.Enabled = true;
// 停止所有客户端连接
foreach (var item in socketList)
{
Socket socket = item.Value;
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
socketList.Clear(); // 清空字典
// 关闭客户端socket
socket.Close();
socket = null;
}
private void addRichTextBox(string text)
{
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText(text);
// 让rich滚动到最下面
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.ScrollToCaret();
}));
}
使用Socket编写客户端程序连接TCP服务器
Socket socket;
private void openBtn_Click(object sender, EventArgs e)
{
openBtn.Enabled = false;
closeBtn.Enabled = true;
string ip = ipTextBox.Text;
string port = portTextBox.Text;
// 1.创建 socket 链接
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 2.链接指定TCP服务器的端口
socket.Connect(new IPEndPoint(IPAddress.Parse(ip), int.Parse(port)));
startReceive();
}
private void startReceive()
{
Task.Run(() =>
{
byte[] receiveData = new byte[1024];
while (true)
{
// 3. 客户端接收服务器返回的数据
try
{
int length = socket.Receive(receiveData);
if(length == 0)
{
addRichTextBox("服务器断开连接");
throw new Exception("服务器断开连接");
}
string msg = Encoding.Default.GetString(receiveData, 0, length);
addRichTextBox($"服务器发送了信息:{msg}\r\n");
}
catch (Exception ex)
{
socket.Close();
BeginInvoke(new Action(() =>
{
openBtn.Enabled = true;
closeBtn.Enabled = false;
}));
break;
}
}
});
}
// 4 向服务器发送消息
private void sendBtn_Click(object sender, EventArgs e)
{
// 把字符串转换为 byte[](批注:一个汉字会变成3个字节。一个数字或字母会变成1个字节;都是一堆数字)
byte[] bytes = Encoding.Default.GetBytes(msgTextbox.Text);
socket.Send(bytes);
}
// 关闭客户端连接
private void closeBtn_Click(object sender, EventArgs e)
{
openBtn.Enabled = true;
closeBtn.Enabled = false;
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
private void addRichTextBox(string text)
{
richTextBox1.Invoke(new Action(() =>
{
richTextBox1.AppendText(text);
// 让rich滚动到最下面
richTextBox1.SelectionStart = richTextBox1.Text.Length;
richTextBox1.ScrollToCaret();
}));
}