界面:
目录
整体思路
服务器端调用socket()通信过程:
socket()—>bind()—>listen()—>accept()—>recv()/recvfrom()—>send()/sendto();
客户端调用socket()通信过程:
socket()—>connect()—>recv()/recvfrom()—>send()/sendto();
服务器端
启动服务的功能
1. 创建socket
本项目采取:InterNetwork寻址方式,流式传输,tcp协议
用到头using System.Net.Sockets;
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2. 绑定端口ip
用到头:using System.Net;
socket.Bind(new IPEndPoint(IPAddress.Parse(IPText.Text), int.Parse(IbText.Text)));
IPAdress:将字符串转成ip地址
IPEndPoint类包含应用程序连接到主机上的服务所需的主机和端口信息,通过组合服务的主机IP地址和端口号,IPEndPoint类形成到服务的连接点。
在IPEndPoint类中有两个很有用的构造函数:
public IPEndPoint(long, int);
public IPEndPoint(IPAddress, int);
它们的作用就是用指定的地址和端口号初始化IPEndPoint类的新实例。
Bind():将创建的socket绑定到指定的IP地址和端口上
3.开始侦听
意思是:同时来了100链接请求,只能处理一个链接,队列里放是个等待链接的客户端,其他返回错误消息
socket.Listen(10);
4.开始接受客户端连接
开启新线程,不断接受客户端连接。
ThreadPool.QueueUserWorkItem方法在线程池中创建一个线程池线程来执行指定的方法(用委托WaitCallback来表示),并将该线程排入线程池的队列等待执行。
用到的头:using System.Threading;
//开始接受客户端链接
ThreadPool.QueueUserWorkItem(new WaitCallback(this.AcceptClientConnect),socket);
...
//窗体全局
List<Socket> ClientProxSocketList = new List<Socket>();
//委托方法:客户端链接
public void AcceptClientConnect(object socket)
{
//强制转化
var serverSocket=socket as Socket;
this.AppendText("服务器端开始接受用户端链接。");
while(true)
{
//代理socket供全局访问,只要一个socket连接上就放入集合中
var proxSocket = serverSocket.Accept();
this.AppendText(string.Format("客户端:{0}连接上了",proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Add(proxSocket);
//不停接受当前链接的客户端消息
//ThreadPool.QueueUserWorkItem(ReceiveData,proxSocket);可以直接这么写,系统自动补全语法糖
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData),proxSocket);
}
}
5. 启动服务代码
List<Socket> ClientProxSocketList = new List<Socket>();
private void Startbtn_Click(object sender, EventArgs e)
{
//创建socket,寻址方式,流式
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定端口ip
socket.Bind(new IPEndPoint(IPAddress.Parse(IPText.Text), int.Parse(IbText.Text)));
//开始侦听
socket.Listen(10);//链接:同时来了100链接请求,只能处理一个链接,队列里放是个等待链接的客户端,其他返回错误
//开始接受客户端链接
ThreadPool.QueueUserWorkItem(new WaitCallback(this.AcceptClientConnect),socket);
}
//委托方法
//委托方法:客户端链接
public void AcceptClientConnect(object socket)
{
//强制转化
var serverSocket=socket as Socket;
this.AppendText("服务器端开始接受用户端链接。");
while(true)
{
//代理socket供全局访问,只要一个socket连接上就放入集合中
var proxSocket = serverSocket.Accept();
this.AppendText(string.Format("客户端:{0}连接上了",proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Add(proxSocket);
//不停接受当前链接的客户端消息
//ThreadPool.QueueUserWorkItem(ReceiveData,proxSocket);可以直接这么写,系统自动补全语法糖
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData),proxSocket);
}
}
发送消息功能
对客户端链接进行遍历,依次进行编码发送。
private void Sendbtn_Click(object sender, EventArgs e)
{
foreach (var proxSocket in ClientProxSocketList)
{
if (proxSocket.Connected)
{
byte[] data = Encoding.Default.GetBytes(msgIn.Text);
proxSocket.Send(data, 0, data.Length, SocketFlags.None);
}
}
}
接收数据功能
在链接客户端的时候,需要同时接收客户端消息,这里再次设置一个线程池和委托方法。接收完数据,我们需要在文本框内追加数据。
1. 将接收的数据放入文本框
socket.Receive():receive函数返回接受到的字节数。若小于等于0,说明客户端退出,将该客户端移除。
socket.RemoteEndPoint : RemoteEndPoint 属性将获取 EndPoint ,其中包含连接到的远程 IP 地址和端口号 Socket 。
此外还需要考虑健壮性,因此要添加异常处理。
//AcceptClientConnect函数的循环里,不停接受当前链接的客户端消息
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData),proxSocket);
//委托方法:接受客户端消息
public void ReceiveData(object socket)
{
var proxSocket = socket as Socket;
byte[] data = new byte[1024*1024];
while (true)
{
int len = 0;
try
{
len = proxSocket.Receive(data, 0, data.Length, SocketFlags.None);
}
catch
{
//客户端异常退出
AppendText(String.Format("客户端:{0}非正常退出", proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Remove(proxSocket);
return;//让方法结束,终结接受客户端数据的异步线程。
}
if(len<=0)
{
//客户端正常退出
AppendText(String.Format("客户端:{0}正常退出", proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Remove(proxSocket);
return;//让方法结束,终结接受客户端数据的异步线程。
}
//把接受到的数据放入文本框
string str =Encoding.Default.GetString(data, 0, len);
//这边被我加了个this用于调试的,不知道加不加this有没有区别
this.AppendText(String.Format("接收到客户端:{0}的消息是:{1}",proxSocket.RemoteEndPoint.ToString(),str));
}
}
2. 自写AppendText函数
在这里新写一个函数用来往文本框追加数据。
考虑到跨线程访问,此时需要用到 InvokeRequired属性。
控件的InvokeRequired属性
bool值,为true时表示调用Send方法的是另一个线程,此时需要将Send方法传送给一个委托(invoke())让委托所在的线程来代替执行Send方法;为false时表示Send的调用者没有跨线程调用Send方法,此时直接执行else中的代码即可。
Control.Invoke 方法 (Delegate) :在拥有此控件的基础窗口句柄的线程上执行指定的委托。
Control.BeginInvoke 方法 (Delegate) :在创建控件的基础句柄所在线程上异步执行指定委托。
两者的区别感觉这个大佬讲得挺好的 :C#.NET:Invoke和BeginInvoke的一些看法(上篇)_兲倥咹净_新浪博客
//往文本框追加数据
public void AppendText(string txt)
{
//考虑跨线程访问
if(Logtxt.InvokeRequired)
{
Logtxt.BeginInvoke(new Action<string>(s => {
this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
}), txt);
//传入字符串同步方法,容易造成线程阻塞,请求时间长
//Logtxt.Invoke(new Action<string>(s => {
// this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
//}), txt);
}
else
{
this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
}
}
服务器端代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
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 chatweb
{
public partial class Form1 : Form
{
List<Socket> ClientProxSocketList = new List<Socket>();
public Form1()
{
InitializeComponent();
}
private void Startbtn_Click(object sender, EventArgs e)
{
//创建socket,寻址方式,流式
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定端口ip
socket.Bind(new IPEndPoint(IPAddress.Parse(IPText.Text), int.Parse(IbText.Text)));
//开始侦听
socket.Listen(10);//链接:同时来了100链接请求,只能处理一个链接,队列里放是个等待链接的客户端,其他返回错误
//开始接受客户端链接
ThreadPool.QueueUserWorkItem(new WaitCallback(this.AcceptClientConnect),socket);
}
//委托方法:客户端链接
public void AcceptClientConnect(object socket)
{
//强制转化
var serverSocket=socket as Socket;
this.AppendText("服务器端开始接受用户端链接。");
while(true)
{
//代理socket供全局访问,只要一个socket连接上就放入集合中
var proxSocket = serverSocket.Accept();
this.AppendText(string.Format("客户端:{0}连接上了",proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Add(proxSocket);
//不停接受当前链接的客户端消息
//ThreadPool.QueueUserWorkItem(ReceiveData,proxSocket);可以直接这么写,系统自动补全语法糖
ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData),proxSocket);
}
}
//委托方法:接受客户端消息
public void ReceiveData(object socket)
{
var proxSocket = socket as Socket;
byte[] data = new byte[1024*1024];
while (true)
{
int len = 0;
try
{
len = proxSocket.Receive(data, 0, data.Length, SocketFlags.None);
}
catch
{
//客户端异常退出
AppendText(String.Format("客户端:{0}非正常退出", proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Remove(proxSocket);
return;//让方法结束,终结接受客户端数据的异步线程。
}
if(len<=0)
{
//客户端正常退出
AppendText(String.Format("客户端:{0}正常退出", proxSocket.RemoteEndPoint.ToString()));
ClientProxSocketList.Remove(proxSocket);
return;//让方法结束,终结接受客户端数据的异步线程。
}
//把接受到的数据放入文本框
string str =Encoding.Default.GetString(data, 0, len);
//这边被我加了个this用于调试的
this.AppendText(String.Format("接收到客户端:{0}的消息是:{1}",proxSocket.RemoteEndPoint.ToString(),str));
}
}
//往文本框追加数据
public void AppendText(string txt)
{
//考虑跨线程访问
if(Logtxt.InvokeRequired)
{
Logtxt.BeginInvoke(new Action<string>(s => {
this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
}), txt);
//传入字符串同步方法,容易造成线程阻塞,请求时间长
//Logtxt.Invoke(new Action<string>(s => {
// this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
//}), txt);
}
else
{
this.Logtxt.Text = string.Format("{0}\r\n{1}", txt, Logtxt.Text);
}
}
private void Sendbtn_Click(object sender, EventArgs e)
{
foreach (var proxSocket in ClientProxSocketList)
{
byte[] data = Encoding.Default.GetBytes(msgIn.Text);
proxSocket.Send(data,0,data.Length,SocketFlags.None);
}
}
}
}