1擂橘、介紹
上一章節(jié)是用unity制作客戶端,Python制作服務(wù)器的簡單Echo程序摩骨。接下來這一章節(jié)是編寫簡單的聊天室程序通贞。
2、客戶端
客戶端的界面如下:
最上面那個黑色帶滾動條的就是聊天窗口。
聊天窗口的制作步驟比較多邪铲,大家可以參考下面的鏈接罢屈,步驟很詳細(xì):
https://zhuanlan.zhihu.com/p/33583772
其中有一個坑的就是如果設(shè)置了text的content size fitter之后,文本窗口會變成居中茎用;這個時候只要將文本的軸點(Pivot )設(shè)置為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];
// 定義服務(wù)器套接字
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;
// 接收的數(shù)據(jù)長度
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;
}
// 發(fā)送數(shù)據(jù)
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 發(fā)送的數(shù)據(jù)長度
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程序不同的只是將發(fā)送的消息和接收到的消息都打印在聊天窗口上羡滑。
在Update中注釋的代碼菇爪,Canvas.ForceUpdateCanvases();就是立刻更新所有Canvases的content;scrollRect.verticalNormalizedPosition = 0f;是將滾動欄滾動到最低部柒昏。不立即調(diào)用是因為空間的寫入值需要一定的時間繪制凳宙,此時滾動條的位置不確定,需要等待繪制完成才調(diào)用职祷。用注釋中的代碼或者在LateUpdate中調(diào)用都行氏涩。或者參考[1]這樣做有梆。都是可以的削葱。
實現(xiàn)的效果為:
3、服務(wù)端
服務(wù)器代碼用Python編寫淳梦,用的Select模型析砸,主要做的事情就是將一個客戶端發(fā)送來的消息遍歷發(fā)送到其它所有客戶端中。直接上代碼:
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爆袍、效果
在不同的客戶端都能夠看到消息首繁。本地客戶端發(fā)送的消息是綠色,其它客戶端發(fā)送的消息是白色陨囊。實現(xiàn)了簡單的聊天功能弦疮,沒有什么其它額外的功能,只是作為一個最簡單的聊天室蜘醋。
5胁塞、結(jié)語
KEEP LEARNING。
附錄
客戶端實現(xiàn)的代碼是用的C#異步API压语,還可以使用poll狀態(tài)監(jiān)測和select模型啸罢。因為對于客戶端而言一直在主線程進(jìn)行檢測,消耗的性能較高胎食,因此多用異步扰才。這里就只附上代碼:
Echo1.cs
using UnityEngine;
using UnityEngine.UI;
using System;
using System.Net;
using System.Net.Sockets;
// 使用Socket的狀態(tài)監(jiān)測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;
// 定義服務(wù)器套接字
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;
}
// 發(fā)送數(shù)據(jù)
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 發(fā)送的數(shù)據(jù)長度
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<Socket> readfds = new List<Socket>();
// 定義服務(wù)器套接字
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;
}
// 發(fā)送數(shù)據(jù)
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(username + ": " + sendMsg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallBack, socket);
showText.text = showText.text + "<color=green>" + username + ": " + sendMsg + "</color>\n";
inputField_msg.text = "";
}
public void SendCallBack(IAsyncResult _ar)
{
try
{
Socket socket = (Socket)_ar.AsyncState;
// 發(fā)送的數(shù)據(jù)長度
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;
}
}
}