1、介绍
上一章节是用unity制作客户端,Python制作服务器的简单Echo程序。接下来这一章节是编写简单的聊天室程序。
2、客户端
客户端的界面如下:
最上面那个黑色带滚动条的就是聊天窗口。
聊天窗口的制作步骤比较多,大家可以参考下面的链接,步骤很详细:
https://zhuanlan.zhihu.com/p/33583772
其中有一个坑的就是如果设置了text的content size fitter之后,文本窗口会变成居中;这个时候只要将文本的轴点(Pivot )设置为x=0,y=0即可。
其他用到的组件都很平常,4个InputField,两个按键。
客户端代码:
Echo.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;
// 使用异步API BeginXXX
public class Echo : MonoBehaviour
{
public InputField inputField_ip;
public InputField inputField_port;
public InputField inputField_msg;
public InputField inputField_username;
public Text showText;
public ScrollRect scrollRect;
private IPAddress ip;
private int port;
private bool updateUI = false;
private string message;
private string username;
private byte[] recvBuff = new byte[1024];
// 定义服务器套接字
Socket socket;
// 输入ip
public void InputIP(string _ip)
{
ip = IPAddress.Parse(inputField_ip.text);
}
// 输入port
public void InputPort(string _port)
{
port = int.Parse(inputField_port.text);
}
public void InputUsername(string _name)
{
username = inputField_username.text;
}
public void OnButtonConnectClick()
{
socket.BeginConnect(ip, port, ConnectCallBack, socket);
}
public void ConnectCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
socket.EndConnect(_ar);
Debug.Log("Connect server successfully.");
socket.BeginReceive(recvBuff, 0, recvBuff.Length, 0, ReceiveCallBack, socket);
}
catch (SocketException ex)
{
Debug.Log("Connect server failed. " + ex.ToString());
}
}
public void ReceiveCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 接收的数据长度
int count = socket.EndReceive(_ar);
string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
message = recvMsg;
updateUI = true;
Debug.Log("Receive message: " + recvMsg);
socket.BeginReceive(recvBuff, 0, recvBuff.Length, 0, ReceiveCallBack, socket);
}
catch(SocketException ex)
{
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
public void OnButtonSendClick()
{
string sendMsg = inputField_msg.text;
if (sendMsg == "")
{
Debug.Log("message not null");
return;
}
// 发送数据
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "" + username + ": " + sendMsg + " \n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 发送的数据长度
int count = socket.EndSend(_ar);
}
catch(SocketException ex)
{
Debug.Log("Send message failed. " + ex.ToString());
}
}
void Start()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
void Update()
{
if(updateUI)
showText.text = showText.text + message + '\n';
//Canvas.ForceUpdateCanvases();
//scrollRect.verticalNormalizedPosition = 0f;
//Canvas.ForceUpdateCanvases();
}
private void LateUpdate()
{
if(updateUI)
{
scrollRect.verticalNormalizedPosition = 0f;
updateUI = false;
}
}
}
上述代码中的BeginConnect,BeginReceive和BeginSend都是Connect、Receive和Send的异步版本。(其实就是在另一条线程上做这个事情)
所做的处理和之前的Echo程序不同的只是将发送的消息和接收到的消息都打印在聊天窗口上。
在Update中注释的代码,Canvas.ForceUpdateCanvases();就是立刻更新所有Canvases的content;scrollRect.verticalNormalizedPosition = 0f;是将滚动栏滚动到最低部。不立即调用是因为空间的写入值需要一定的时间绘制,此时滚动条的位置不确定,需要等待绘制完成才调用。用注释中的代码或者在LateUpdate中调用都行。或者参考[1]这样做。都是可以的。
实现的效果为:
3、服务端
服务器代码用Python编写,用的Select模型,主要做的事情就是将一个客户端发送来的消息遍历发送到其它所有客户端中。直接上代码:
demo.py
import socket
import select
import ClientStates as cs
# server socket
ip = "127.0.0.1"
port = 8888
server_address = (ip, port)
buffer_size = 1024
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(server_address)
server.listen(5)
connect_socket = [server]
client_states = {}
print "Server Start."
# Main Loop
while True:
read_fds, write_fds, error_fds = select.select(connect_socket, [], [], 1)
for fd in read_fds:
if fd is server:
# client connect
client_socket, client_address = fd.accept()
print client_address, 'connected'
connect_socket.append(client_socket)
client_states[client_socket] = cs.ClientStates(client_socket, client_address)
else:
data = fd.recv(buffer_size)
if data:
print 'receive data from: ', client_states[fd].addr
for client in client_states.values():
if client.socket is fd:
continue
client.socket.send(data)
else:
connect_socket.remove(fd)
client_states.pop(fd)
fd.close()
ClientSates.py
class ClientStates(object):
def __init__(self, sock, address):
self.socket = sock
self.addr = address
self.recv_buff = []
4、效果
在不同的客户端都能够看到消息。本地客户端发送的消息是绿色,其它客户端发送的消息是白色。实现了简单的聊天功能,没有什么其它额外的功能,只是作为一个最简单的聊天室。
5、结语
KEEP LEARNING。
附录
客户端实现的代码是用的C#异步API,还可以使用poll状态监测和select模型。因为对于客户端而言一直在主线程进行检测,消耗的性能较高,因此多用异步。这里就只附上代码:
Echo1.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;
// 使用Socket的状态监测Poll
public class Echo1 : MonoBehaviour
{
public InputField inputField_ip;
public InputField inputField_port;
public InputField inputField_msg;
public InputField inputField_username;
public Text showText;
public ScrollRect scrollRect;
private IPAddress ip;
private int port;
private bool updateUI = false;
private string username;
// 定义服务器套接字
Socket socket;
// 输入ip
public void InputIP(string _ip)
{
ip = IPAddress.Parse(inputField_ip.text);
}
// 输入port
public void InputPort(string _port)
{
port = int.Parse(inputField_port.text);
}
public void InputUsername(string _name)
{
username = inputField_username.text;
}
public void OnButtonConnectClick()
{
// 对于connect不必要用异步连接, 使用同步connect
socket.Connect(ip, port);
}
public void OnButtonSendClick()
{
string sendMsg = inputField_msg.text;
if (sendMsg == "")
{
Debug.Log("message not null");
return;
}
// 发送数据
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "" + username + ": " + sendMsg + " \n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 发送的数据长度
int count = socket.EndSend(_ar);
}
catch (SocketException ex)
{
Debug.Log("Send message failed. " + ex.ToString());
}
}
void Start()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
void Update()
{
if (socket == null)
return;
if (socket.Poll(0, SelectMode.SelectRead))
{
// socket可读
byte[] recvBuff = new byte[1024];
int count = socket.Receive(recvBuff);
string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
showText.text = showText.text + recvMsg + '\n';
updateUI = true;
}
}
private void LateUpdate()
{
if (updateUI)
{
scrollRect.verticalNormalizedPosition = 0f;
updateUI = false;
}
}
}
Echo2.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;
// 使用Select模型
public class Echo2 : MonoBehaviour
{
public InputField inputField_ip;
public InputField inputField_port;
public InputField inputField_msg;
public InputField inputField_username;
public Text showText;
public ScrollRect scrollRect;
private IPAddress ip;
private int port;
private bool updateUI = false;
private string username;
List readfds = new List();
// 定义服务器套接字
Socket socket;
// 输入ip
public void InputIP(string _ip)
{
ip = IPAddress.Parse(inputField_ip.text);
}
// 输入port
public void InputPort(string _port)
{
port = int.Parse(inputField_port.text);
}
public void InputUsername(string _name)
{
username = inputField_username.text;
}
public void OnButtonConnectClick()
{
// 对于connect不必要用异步连接, 使用同步connect
socket.Connect(ip, port);
}
public void OnButtonSendClick()
{
string sendMsg = inputField_msg.text;
if (sendMsg == "")
{
Debug.Log("message not null");
return;
}
// 发送数据
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "" + username + ": " + sendMsg + " \n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 发送的数据长度
int count = socket.EndSend(_ar);
}
catch (SocketException ex)
{
Debug.Log("Send message failed. " + ex.ToString());
}
}
void Start()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
void Update()
{
if (socket == null)
return;
readfds.Clear();
readfds.Add(socket);
// Select模型
Socket.Select(readfds, null, null, 1);
// 遍历可读的socket --- 其实就只有自己的socket
foreach (Socket fd in readfds)
{
byte[] recvBuff = new byte[1024];
int count = fd.Receive(recvBuff);
string recvMsg = System.Text.Encoding.UTF8.GetString(recvBuff, 0, count);
showText.text = showText.text + recvMsg + '\n';
updateUI = true;
}
}
private void LateUpdate()
{
if (updateUI)
{
scrollRect.verticalNormalizedPosition = 0f;
updateUI = false;
}
}
}
参考:
[1] https://zhuanlan.zhihu.com/p/33583772