搭建方式主要可以分为同步与异步两种。同步相对来说会更简单直接暴力,但因为会造成线程阻塞,所以适用性不高,但从学习入门角度来看,可以更简单直接地了解整个过程。
同步Socket程序:
服务器遵照Socket基本流程,首先创建Socket,再调用Bind绑定IP和Port,然后调用Listen等待客户端连接,最后在一个无限循环中调用Accept接收客户端,并且再进行后序操作。大致的代码如下:
class ServerTest
{
static void Main(string[] args)
{
//Socket
Socket listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint ep = new IPEndPoint(ip, 8742);
listenfd.Bind(ep);
//Listen
listenfd.Listen(0); // 0代表当连接数满了的时候的菊花队列
Console.WriteLine("服务器启动成功");
while(true)
{
//Accept
Socket conn = listenfd.Accept();
Console.WriteLine("服务器接收客户端");
//Receive
byte[] readBuff = new byte[1024];
int count = conn.Receive(readBuff);
string str = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
Console.WriteLine("服务器接收信息成功");
//Send,对接收信息稍作修改然后发出给客户端
byte[] bytes = System.Text.Encoding.Default.GetBytes("from server..." + str);
conn.Send(bytes);
}
}
}
---------------------------------------------------------------------
上述代码实现了一个同步通信的过程,但是实用性并不高,当执行某一个线程阻塞的方法时程序就会一直处于卡死状态,直到得到了客户端的回应才会继续执行,这显然不符合使用要求,因此需要使用多线程的方式对此过程进行修改,某些会导致线程阻塞的方法需要开一条新的线程去执行,这样每当辅助线程因调用了某些阻塞方法处于等待状态时,主线程依然可以继续进行其他工作,并不影响程序运行。
异步Socket程序的代码如下:
服务端:
namespace NetworkBaseTest
{
class UserToken
{
public const int BUFFER_SIZE = 1024;
public Socket socket;
public bool isUsed = false;
public byte[] readBuff;//= new byte[BUFFER_SIZE]; 不知有什么意义,这里和构造函数可能冲突了
public int buffCount = 0;
public int BuffRemain
{
get { return BUFFER_SIZE - buffCount; }
}
public string GetAddress
{
get
{
if(!isUsed)
{
return "无法获取地址";
}
return socket.RemoteEndPoint.ToString();
}
}
public UserToken()
{
readBuff = new byte[BUFFER_SIZE];
}
public void Init(Socket socket)
{
this.socket = socket;
isUsed = true;
buffCount = 0;
}
public void Close()
{
if (!isUsed) return;
Console.WriteLine("[断开连接]" + GetAddress);
socket.Close();
isUsed = false;
}
}
}
--------------
namespace NetworkBaseTest
{
class Server
{
//监听套接字
public Socket listenfd;
//客户端连接
public UserToken[] tokens;
//最大连接数
public int maxTokens = 50;
//获取连接池索引,返回负数表示获取失败
public int NewIndex
{
get
{
if (tokens == null) return -1;
//此例子运用数组作为容器不够好,每次都要遍历获取可用token,
//如果使用Stack会更合适,因为不需要再遍历
for (int i = 0; i < tokens.Length; i++)
{
if(tokens[i]==null)
{
tokens[i] = new UserToken();
return i;
}
else if(tokens[i].isUsed==false)
{
return i;
}
}
return -1;
}
}
//开启服务器
public void Start(string host, int port)
{
//初始化连接池
tokens = new UserToken[maxTokens];
for (int i = 0; i < maxTokens; i++)
{
tokens[i] = new UserToken();
}
//Socket
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse(host);
IPEndPoint ipEP = new IPEndPoint(ipAdr, port);
listenfd.Bind(ipEP);
//Listen
listenfd.Listen(10); //当连接数满了后,能够处于菊花阵的人数为10人队列
//Accept
listenfd.BeginAccept(AcceptCb, null); //这里的null为什么不是listenfd自身
Console.WriteLine("[服务器] 启动成功");
}
/// <summary>
/// Accept 回调 当服务器Accept到客户端的时候,就会回调此函数
/// </summary>
/// <param name="ar"></param>
private void AcceptCb(IAsyncResult ar)
{
try
{
//名为listenfd的Socket对象永远只负责Accept客户端,每次Accept到一个都会返回一个新的Socket
//也就是通过EndAccept方法返回,我们只能通过这个返回的Socket对象来获取客户端信息
Socket socket = listenfd.EndAccept(ar);
//在tokens池中寻找空余位置供客户端使用
int index = NewIndex;
if(index<0)
{
socket.Close();
Console.WriteLine("[警告] 连接已满");
}
else
{
UserToken token = tokens[index];
token.Init(socket);
string adr = token.GetAddress;
Console.WriteLine("客户端连接 ["+adr+"] tokens池ID:" +index);
//开始异步接收客户端消息
token.socket.BeginReceive(token.readBuff, token.buffCount, token.BuffRemain, SocketFlags.None, ReceiveCb, token);
}
//初步推断此方案应该是服务器接收了客户端后,就把信息保存在tokens池中
//然后重新调用BeginAccept方法继续工作,按照这种流程应该是一个一个地来接收的
//因此同一时间只有一个BeginAccept在执行
listenfd.BeginAccept(AcceptCb, null);
}
catch(Exception e)
{
Console.WriteLine("AcceptCb失败:"+e.Message);
}
}
//Receive 回调
private void ReceiveCb(IAsyncResult ar)
{
UserToken token = ar.AsyncState as UserToken;
try
{
int count = token.socket.EndReceive(ar);
//关闭信号 约定了如果能接收信息,但是长度为0的话就是客户端的断开信号
if(count<=0)
{
Console.WriteLine("收到 [" + token.GetAddress + "] 断开连接");
token.Close();
return;
}
//数据处理
//收到了客户端1的消息
string str = Encoding.UTF8.GetString(token.readBuff, 0, count);
Console.WriteLine("收到 [" + token.GetAddress + "] 数据:" + str);
str = token.GetAddress + ":" + str;
byte[] bytes = Encoding.Default.GetBytes(str);
//广播
//把客户端1的消息转发给所有的客户端
for (int i = 0; i < tokens.Length; i++)
{
if (tokens[i] == null)
continue;
if (!tokens[i].isUsed)
continue;
Console.WriteLine("将消息转播给"+tokens[i].GetAddress);
tokens[i].socket.Send(bytes);
}
//继续接收 (类似递归)
token.socket.BeginReceive(token.readBuff, token.buffCount, token.BuffRemain, SocketFlags.None, ReceiveCb, token);
}
catch(Exception e)
{
Console.WriteLine("收到 ["+token.GetAddress+"] 断开连接");
token.Close();
}
}
}
}
------------
namespace NetworkBaseTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Server server = new Server();
string ip = "127.0.0.1";
int port = 1889;
server.Start(ip, port);
while(true)
{
string str = Console.ReadLine();
switch(str)
{
case "quit":
return;
}
}
}
}
}
------------------
客户端:
客户端的代码在Unity中实现:以网络聊天室为例子
public class ClientNet : MonoBehaviour
{
//服务器IP和端口
public InputField input_host;
public InputField input_port;
//显示客户端收到的消息
public Text txt_recv;
public string recv;
//显示客户端IP和端口
public Text txt_clientip;
//聊天输入框
public InputField input_text;
//Socket和接收缓冲区
private Socket socket;
private const int BUFFER_SIZE = 1024;
public byte[] readBuff = new byte[BUFFER_SIZE];
//C#使用线程池处理异步调用,所以ReceiveCb并不在主线程中,但只有主线程可以
//设置UI组件,因此ReceiveCb只设置字符串recv,再由主线程Update方法处理UI组件
private void Update()
{
txt_recv.text = recv;
}
public void OnConnectClick()
{
//清理text
txt_recv.text = "";
//Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Connect
string host = input_host.text;
int port = int.Parse(input_port.text);
socket.Connect(host, port);
txt_clientip.text = socket.LocalEndPoint.ToString();
//recv
socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);
}
//异步接收回调
private void ReceiveCb(IAsyncResult ar)
{
try
{
//count 是接收数据的大小
int count = socket.EndReceive(ar);
//数据处理
string str = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
//当内容框到达上限是,自动清空
if (recv.Length > 300) recv = "";
recv += str + "\n";
//继续接收
socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);
}
catch(Exception e)
{
txt_recv.text += "连接已断开";
socket.Close();
}
}
public void Send()
{
string str = input_text.text;
byte[] bytes = System.Text.Encoding.Default.GetBytes(str);
try
{
socket.Send(bytes);
}
catch { }
}
}
-----------------------------------------------------
关于异步Socket的实现方法不止一种,也可以通过自行封装SocketAsyncEventArgs构建出其他流程。
异步通信的大致流程如上图所示