如有错误或者误区,欢迎大家指出,共同进步~
架构:
server端使用vs2015写的控制台程序,server socket bind本地的ip,端口自定义,使用映射服务软件将server程序的端口映射到公网,便于client端在公网上进行访问。server程序在自己的电脑上运行,如果你的资金宽裕,你完全可以在各种云平台租用云主机,而放弃使用映射软件,不过其中的环境问题实在是有意思,比如,博主之所以选择使用映射端口,就是因为在另一个开发中server端需要使用sqlserver数据库,但是阿里云的学生机实在是谁用谁知道,sqlserver2008表示在windows server2012 R2系统实在安不上去,所以不如使用自己熟悉的电脑来做服务器,而且花费少了几十倍。(虽然自己的电脑需要一直开机不断网)。
client端使用unity 3d 5.3.5f1开发,client端通过域名连接server程序的端口,基于unity的client可以极简单的实现client端跨平台,目前只做了pc和andriod端。
client:
1.安装Android SDK,在unity上导入NGUI插件,使用NGUI进行UI界面的开发。
2.新建工程socket_client
3.新建scene1,在sence1内对server端进行连接
搭建一个简单的UI界面进行提示,建立一个空GameObject,Netmanger,在上面挂载一个Client_mannger脚本管理client内的所有逻辑。

Client_mannger脚本如下:
(1).使用单例模式,进行跨scene的调用
(2).定义套接字client_Link,在Start方法里进行连接,在这里使用Dns.Resolve方法建立server域名的连接
(3).对象label1(net_massage),进行提示,如果连接完成,输出“服务器连接成功”,如果连接失败不会进行(4),会在这里出现异常
(4).完成以上连接后,开启异步接收接收来自server的数据,再激活Start_Button,进入sence2
(5).关于异步回调:
在Start方法内开启异步回调方法,将接收到的数据放到r_data数组内,接收到数据后执行CallBack回调方法,关闭这次异步回调,将接收到的byte数组解析成string放到sdata里,对sdata进行判断过滤掉Socket连接的测试空数据(不做这一步的话空字符会影响你需要的信息接收效果),将过滤的信息放到这个类的message字段内(之后会解释),然后将接收数据用的r_data数组滞为空(不影响下一次接受数据),之后在回调方法内再开启下一次的异步回调接收数据,回调函数就是Callback,这样就可以形成不间断接收的循环。
(6).Onclickstart:载入scene2,一定使用 DontDestroyOnLoad(this),避免载入下一个scene的时候不会摧毁这个核心控制器。
(7).SendMes:进行信息发送,添加特殊标记“#1”表示这个是聊天信息,进行拓展的时候,可以添加别的sign,在server进行处理。
using UnityEngine;
using System.Net.Sockets;
using System.Net;
using System;
using System.Text;
public class client_mannger : MonoBehaviour {
Socket client_Link = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
public GameObject Lable1;
public GameObject Start_Button;
public UIInput input;
public static client_mannger Net_Ob;
private byte[] temp = new byte[1024];//null
private byte[] r_data = new byte[1024];//接收数组
private byte[] s_data = new byte[1024];//发送数组
public string message="";
// Use this for initialization
void Start()
{
Net_Ob = this;
//目标ip
IPHostEntry test = Dns.Resolve("13062e54.nat123.net");
client_Link.Connect(test.AddressList[0], 20365);
Lable1.GetComponent<UILabel>().text = "服务器连接完成";
client_Link.BeginReceive(r_data, 0, 1024, SocketFlags.None, new AsyncCallback(CallBack), client_Link);
Start_Button.gameObject.SetActive(true);
}
void CallBack(IAsyncResult re)
{
Socket Link = re.AsyncState as Socket;
Link.EndReceive(re);
string sdata=Encoding.UTF8.GetString(this.r_data);//接收临时字符串
if(Convert.ToInt32(sdata[2])>0)//过滤Socket连接测试字符
{
this.message = sdata;
}
this.r_data = temp;
client_Link.BeginReceive(r_data, 0, 1024, SocketFlags.None, new AsyncCallback(CallBack), Link);
}
public void Onclickstart()
{
DontDestroyOnLoad(this);
Application.LoadLevel("sence2");
}
public void SendMes()
{
this.s_data = Encoding.UTF8.GetBytes("#1"+this.input.value);
this.input.value = "";
client_Link.Send(s_data);
}
}
挂载在Net_mannger上的Client_mannger脚本:

Lable 1:提示网络连接状态
Start_Button:切换到下一个场景
Input:用于在scene2里接收用户的输入
Message:储存通信接收到的合法信息
如果连接正常,如下图所示:

4.新建scene2,在这里进行聊天
使用NGUI搭建一个简单的UI界面用于聊天通信

新建一个空对象挂载object_mannger脚本,控制对象,显示聊天信息等(在这里对server发来的信息进行解析,可以拓展多种命令,不只是显示聊天信息)
object_mannger脚本如下:
(1).载入scene2时,对client_mannger里的input对象进行赋值
(2).声明一个GameObject数组里管理用于显示信息的label
(3).写一个Send方法来调用已经在client_mannger脚本里写好的发送信息方法
(4).信息显示:
在这里会遇到层出不穷的bug,下面说两个重大的bug,其他小问题就不说了,细说bug。。。
1. 线程安全:具体的error忘了留下了,原因就是在异步接收信息的回调方法里直接写了关于显示信息这一块,unity引擎的规则之一,不能在非main Thread里调用程序里的UI对象,所以在接收信息的client_mannger类里声明了一个message的string对象,当异步线程完成接收信息之后,将信息赋给这个字段。然后在object_mannger的主线程(Update方法)里监测这个对象,当信息不为空时就能在主线程里进行信息的显示。就不会引起线程安全问题。
2.label的显示bug:程序不会报错,你的client端可以接受到来自server端的信息,如果你每次只是简单的使用label.text+=("接收到的信息"+\n);client端的label显示的text就会间接性的消失(时而正常工作时而罢工)博主测试得到的结果是,如果是该客户端发送的信息,该客户端能够很好的接收自己发到server再由server返回的信息,但是其他的clent就出现上面所说的情况。后来放弃了简单的 (label.text+=)方法,改为用动态生成的label来显示每一次接收的信息就能完美工作了。
(5).label数组管理:
1.先在该类里声明改造一个GameObject数组,在主线程的Update方法里进行监测client_mannger脚本里的message字段是否已经不为空(接收到合法数据)
2.对合法信息的命令标示符进行解析,这里用“#1”表示接受到的是用于需要显示的聊天信息,解析的方法是字符的ASC2码,index是合法信息的0和1两个字符的ASC2码之和,‘#’+‘1’=84,表示聊天信息
3.使用一个for循环将label数组里的所有对象向上移动到合适的位置,留出显示新接收到的信息的位置,在这里博主遇到一个小bug,所有的label条移不动,并且动态生成的新的label永远出现在(0,0,0)的位置,原因是使用了脚本里注释的Initantiate方法初始化label对象,改为使用NGUITools.AddChild方法就可以正常工作了
4.截取命令符之后的有效字符串,初始化label并且显示信息,添加到label数组里面
5.做完这些以后千万要记得将client_mannger脚本里的message字符串置为空,表示已经结束完成了一次server发来的命令,不然死循环执行这个任务
using UnityEngine;
using System.Collections;
using System;
using System.Collections.Generic;
public class object_manager : MonoBehaviour {
public UIInput input;
public GameObject lable;
public GameObject root;
public List<GameObject> labelList;
// Use this for initialization
void Start () {
client_mannger.Net_Ob.input = this.input;
labelList = new List<GameObject>();
}
public void Send()
{
client_mannger.Net_Ob.SendMes();
}
// Update is called once per frame
void Update () {
if (client_mannger.Net_Ob.message != "" && client_mannger.Net_Ob.message != null)
{
int index = Convert.ToInt32(client_mannger.Net_Ob.message[0]) + Convert.ToInt32(client_mannger.Net_Ob.message[1]);//命令标示序号
if (index == 84)
{
for (int i = 0; i < labelList.Count; i++)
{
labelList[i].GetComponent<Transform>().localPosition = new Vector3(labelList[i].GetComponent<Transform>().localPosition.x,
labelList[i].GetComponent<Transform>().localPosition.y + 15,
labelList[i].GetComponent<Transform>().localPosition.z);
}
string temp = client_mannger.Net_Ob.message.Substring(2, client_mannger.Net_Ob.message.Length - 2);
//UILabel tempGO = Instantiate(lable, lable.transform.position,lable.transform.rotation) as UILabel;
GameObject tempGO = NGUITools.AddChild(root,lable);
tempGO.GetComponent<Transform>().localPosition = new Vector3(-370, -150, 0);
labelList.Add(tempGO);
labelList[labelList.Count - 1].GetComponent<UILabel>().text = temp ;
}
client_mannger.Net_Ob.message = "";
}
}
}
object_mannger空物体挂载的object_mannger脚本:

Input:用户的输入框
lable:用于放置需要动态生成的label的Prefabs
Root:NGUI的根节点(使用NGUITools.AddChild(),需要指明生成UI的根节点)
client的开发工作就完成了,使用这个模型,你可以再拓展上别的命令,比如公告,logon,等等。。。
关于打包就不在这里赘述,网上有太多教程了。。
server:
博主的代码里存在大量的注释代码,是一些以前调试的存余,
1.使用vs2015新建一个控制台工程,当然,你可以建别的工程
2.完成main thread(program.cs)
结构图如下:

代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Collections;
namespace chat_room_server
{
public class Program
{
//建立一个连接列表方便管理连接
public static List<client_Link> LinkList = new List<client_Link>();
static Socket server_socket=new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
static void Main(string[] args)
{
//获取当前主机的IP
/*string Host_Name = Dns.GetHostName();
Console.WriteLine("主机名字:" + Host_Name);
Console.WriteLine("请选择IP序号:");
IPAddress[] IPList = Dns.GetHostAddresses(Host_Name);
foreach(var t in IPList)
{
Console.WriteLine(t);
}
int temp = 1;
//建立服务器结点并绑定
server_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
temp = int.Parse(Console.ReadLine());
Console.WriteLine("选择的绑定IP:"+IPList[temp]);*/
IPAddress IPAd = IPAddress.Parse("0.0.0.0");
IPEndPoint lacolEP = new IPEndPoint(IPAd,2000);
server_socket.Bind(lacolEP);
//服务器置为监听状态最多连接50个client
server_socket.Listen(50);
Console.WriteLine("the server get ready");
//创建一个线程一直等待客户端的连接
Thread wait_concent = new Thread(Waitconcet);
wait_concent.Start();
//为客户端转接数据:放到客户端连接类里
}
public static void Waitconcet(object temp)
{
while (true)
{
Console.WriteLine("wait for connect");
client_Link Link = new client_Link(server_socket.Accept());
Console.WriteLine("a client be connected");
LinkList.Add(Link);
}
}
}
}
3.完成类client_Link(client_Link.cs)
结构图如下:

代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Threading;
namespace chat_room_server
{
public class client_Link
{
public static byte[] data = new byte[1024];
//public static byte[] temp = new byte[1024];
//private Byte[] data=new Byte[1024];
private Socket Link_client;
public client_Link(Socket temp)
{
this.Link_client = temp;
//信息转接
//解决方案一(为每一个客户端建立一个线程用于转接信息)
//Thread Tclient = new Thread(Method);
//Tclient.Start();
//解决方案二(使用异步方法来进行异步接受)
Console.WriteLine("The server prepare for translate data...");
this.Link_client.BeginReceive(data,0,data.Length,SocketFlags.None,new AsyncCallback(SendMessge),this.Link_client);
}
public static void SendMessge(IAsyncResult ar)//方案二
{
try
{
Socket re = ar.AsyncState as Socket;
Console.WriteLine("准备发送一次数据");
Console.WriteLine(Encoding.UTF8.GetString(data));
for(int i=0; i<Program.LinkList.Count;i++)//清除以断开的连接,并广播信息
{
if (Program.LinkList[i].Link_client.Poll(10, SelectMode.SelectRead))
{
Program.LinkList.Remove(Program.LinkList[i]);
}
else
{
Program.LinkList[i].Link_client.Send(data);
}
}
data = new byte[1024];
if (!re.Poll(10, SelectMode.SelectRead))//Socket.Poll() 方法,检测Socket连接是否断开,断开则返回true
{//检测连接没有断开则开启下一次执行
Console.WriteLine("进来了");
re.EndReceive(ar);
re.BeginReceive(data, 0, data.Length, SocketFlags.None, new AsyncCallback(SendMessge), re);
}//递归调用,无限循环
}
catch (SocketException s)
{
Console.WriteLine(s);
Console.WriteLine("服务器出现异常");
//Socket re = ar.AsyncState as Socket;
//re.EndReceive(ar);
}
}
//public void Method(object temp)// 方案一
//{
// while (true)
// {
// Console.WriteLine("server wait for cilent's data.....");
// this.Link_client.Receive(data);
// Console.WriteLine("data transfering...");
// this.Link_client.Send(data);
// }
//}
}
}
然后基本的广播功能server就完成了。
一些留在server端开发后面的话:
(1).用于检测client是否断开的方法不要使用Socket.connected属性,这个属性用不了,使用Socket.Poll()方法就好了
(2).你完全可以使用开启死循环线程的方式来代替异步接收,博主为了锻炼自己使用了异步接收的方式来转接信息
端口映射:
1.确定你的服务器开启端口监听成功
博主server程序Bind的端口是2000,这个可以自定义,选一个计算机内没有被程序占用的端口
查看计算机所有端口工作状态: 在命令行输入netstat -ano

查看计算机中指定端口: 在命令行输入netstat -ano|findstr "在这里写你需要查询的端口号"
博主开启server程序之后,查询2000端口:

listening 表示端口在侦听状态,等待client的连接
2.安装nat123软件,注册登录,充值
(1).添加映射
点击添加映射

(2).添加一个其他(非网站)映射,内网端口写server程序的端口号,别的不用改

(3).然后回到主界面,在主界面这里如下图显示,server程序端口就已经映射到公网了

到了这一步,就可以使用所有可以上网的设备连接到server了
可以拓展更多的功能,只做了一个简简单单的client
本文介绍了使用C#开发TCP Socket公网聊天通信的过程,包括客户端和服务器端的步骤。客户端通过Unity集成NGUI进行UI设计,连接服务器;服务器端在VS2015中创建,完成主要逻辑。此外,还详细讲解了端口映射的设置,确保公网可达。最后,作者提醒存在一些英文错误,并欢迎读者指正。
&spm=1001.2101.3001.5002&articleId=52760760&d=1&t=3&u=2a5a620243b0410d828edd3e193d0357)
1万+

被折叠的 条评论
为什么被折叠?



