C# Socket网络编程之服务端部署
前前后后弄了差不多一个星期,终于把这玩意儿给搞出来了,其实谈不上多复杂,但是从0开始还是要走很多的弯路,现在也没有博客或者视频教程教弄这个东西,所以写下这篇博客方便以后要用的时来看看。
总的来说需要掌握的东西不多。
- C# socket网络编程
- Linux基本命令
- TCP/IP协议
以下是我的服务端代码(由于我是用的winform框架写的,代码有两部分)
program.cs
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Drawing;
namespace ChatoServer
{
static class Program
{
//1.创建套接字
static Socket serverSocket = null;
static IPAddress ip = null;
static IPEndPoint point = null;
static Dictionary<string, Socket> allClientSockets = null;
static MainForm form = null;
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]//开启单线程
//表示这个Main程序被一个单线程套间包住,且Main的执行,一次只能被一个线程占用,这个线程未执行完,别的线程是没办法调用的。
static void Main()
{
//在 C# 中,System.Threading.Thread 类用于线程的工作。它允许创建并访问多线程应用程序中的单个线程。进程中第一个被执行的线程称为主线程。
// 当 C# 程序开始执行时,主线程自动创建。使用 Thread 类创建的线程被主线程的子线程调用。您可以使用 Thread 类的 CurrentThread 属性访问线程
allClientSockets = new Dictionary<string, Socket>();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
form = new MainForm(bListenClick, bSendClick);
Application.Run(form);
}
static EventHandler bListenClick = SetListen;//启动服务器相当于
static EventHandler bSendClick = SendMsg;
static void SetListen(object sender, EventArgs e)
{
ip = IPAddress.Parse(form.GetIPText());//获得IP
point = new IPEndPoint(ip, form.GetPort());//获得端口
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//设置套接字
try {
serverSocket.Bind(point);//绑定
serverSocket.Listen(20);//监听,最多20个
form.Println($"服务器开始在 {point} 上监听。");
Thread thread = new Thread(Listen);
thread.SetApartmentState(ApartmentState.STA);//当前线程状态为单线程
//当初始化一个线程,把Thread.IsBackground=true的时候,指示该线程为后台线程。后台线程将会随着主线程的退出而退出。
thread.IsBackground = true;
thread.Start(serverSocket);//开始线程的的执行
}
catch (Exception ex) {
form.Println($"错误: {ex.Message}");
}
}
static void Listen(object so)
{
Socket serverSocket = so as Socket;
while (true)//不断接收来着客户端的请求
{
try {
//等待连接并且创建一个负责通讯的socket
Socket clientSocket = serverSocket.Accept();
//获取链接的IP地址
string clientPoint = clientSocket.RemoteEndPoint.ToString();
form.Println($"{clientPoint} 上的客户端请求连接。");
allClientSockets.Add(clientPoint, clientSocket);//加入字典--一个IP对应一个套接字。。。。
form.ComboBoxAddItem(clientPoint);//把连接的客户端的IP加入下拉列表方便通讯。。。
string msg = "ip=" + clientPoint;
//设置缓冲区,并把IP:具体IP的字符串设置成UTF-8编码的字节数组。。。
byte[] sendee = Encoding.UTF8.GetBytes(msg);
//相当于Python的range。。。。
foreach (string ip in allClientSockets.Keys)
if (ip != clientPoint)
{
//ip为当前遍历到的IP,clientPoint为新加进来的IP
allClientSockets[ip].Send(sendee); //向所有客户端发送新客户端连接的ip信息
byte[] sendeeIP = Encoding.UTF8.GetBytes("ip=" + ip);
allClientSockets[clientPoint].Send(sendeeIP); //向新客户端发送所有已有客户端的ip信息
}
//开启一个新线程不停接收消息
Thread thread = new Thread(Receive);
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start(clientSocket);
}
catch(Exception e) {
form.Println($"错误: {e.Message}");
break;
}
}
}
static void Receive(object so)
{
//接收缓冲区
byte[] buf = new byte[1024 * 1024 * 2];
byte[] cacheBuf = new byte[1024 * 1024 * 2];
Socket clientSocket = so as Socket;
string clientPoint = clientSocket.RemoteEndPoint.ToString();
while (true) {
try {
//获取发送过来的消息容器
//Socket.Receive返回收到的字节数。
int len = clientSocket.Receive(buf);
//有效字节为0则跳过
if (len == 0) break;
if (buf[0] == 1) //如果接收的字节数组的第一个字节是1,说明接收的是文件
{
form.Println("来自" + clientSocket.RemoteEndPoint + ": 的文件buf正在接收");
Buffer.BlockCopy(buf, 0, cacheBuf, 0, 1024 * 1024 * 2);
Thread thread = new Thread(Receive);
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start(clientSocket);
}
else if(buf[0] == 2)
{
form.Println("来自" + clientSocket.RemoteEndPoint + ": 的图片buf正在接收");
//发送并显示图片
MemoryStream ms = new MemoryStream();
ms.Write(buf, 1, len - 1);
Image img = Image.FromStream(ms);
form.SetPic(img);
form.Println("来自" + clientSocket.RemoteEndPoint + ": 的图片接收成功");
}
else
{
string s = Encoding.UTF8.GetString(buf, 0, len);
form.Println($"{clientPoint}: {s}");
if (s.StartsWith("message="))//判断字符串是否以message=开头
foreach (String t in allClientSockets.Keys)
{
if (clientPoint != t)
{
byte[] sendee = Encoding.UTF8.GetBytes($"{clientPoint}: {s}");
allClientSockets[t].Send(sendee);
}
}
else if (s.StartsWith("ip="))
{
string ip = Regex.Match(s, @"d{1,3}.d{1,3}.d{1,3}.d{1,3}:d{1,5}").Value;
byte[] sendee = Encoding.UTF8.GetBytes($"{clientPoint}: {s.Substring(3+ ip.Length)}");
allClientSockets[ip].Send(sendee);
}
if (s.StartsWith("TO="))
{
string ip = Regex.Match(s, @"d{1,3}.d{1,3}.d{1,3}.d{1,3}:d{1,5}").Value;
allClientSockets[ip].Send(cacheBuf);
}
}
//byte[] sendee = Encoding.UTF8.GetBytes("服务器返回信息");
//clientSocket.Send(sendee);
}
catch (SocketException e) {
allClientSockets.Remove(clientPoint);
form.ComboBoxRemoveItem(clientPoint); //移除服务端相应用户列表项
string msg = "Removeip=" + clientPoint; //给所有客户端发送移除消息
byte[] sendee = Encoding.UTF8.GetBytes(msg);
foreach (string ip in allClientSockets.Keys)
allClientSockets[ip].Send(sendee);
form.Println($"客户端 {clientSocket.RemoteEndPoint} 中断连接: {e.Message}");
clientSocket.Close();
break;
}
catch(Exception e) {
form.Println($"错误: {e.Message}");
}
}
}
static void SendMsg(object sender, EventArgs e)
{
if(form.GetComboBoxItem() == null)
{
string msg = form.GetMsgText();
if (msg == "") return;
byte[] sendee = Encoding.UTF8.GetBytes($"服务器:{msg}");
foreach (Socket s in allClientSockets.Values)
s.Send(sendee);
form.Println(msg);
form.ClearMsgText();
}
else
{
string msg = form.GetMsgText();
if (msg == "") return;
byte[] sendee = Encoding.UTF8.GetBytes($"服务器:{msg}");
Socket socketSend = allClientSockets[form.GetComboBoxItem()];
socketSend.Send(sendee);
form.Println(msg);
form.ClearMsgText();
}
}
}
}
form.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace ChatoServer
{
public partial class MainForm : Form
{
public MainForm(EventHandler bListenClick, EventHandler bSendClick)
{
InitializeComponent();
//这样设计方便代码分离。。。。
this.buttonListen.Click += bListenClick;
this.buttonSend.Click += bSendClick;
}
public string GetIPText()
{
//获取IP地址
return this.textBoxIP.Text;
}
public int GetPort()
{
//获取端口地址
return (int)this.numericUpDownPort.Value;
}
public string GetMsgText()
{
//获取输入框信息
return this.textBoxSendee.Text.Trim();
}
public void SetPic(Image img)
{
//写入图片文件
this.pictureBox1.Image = img;
}
public void ClearMsgText()
{
//清除文本框信息
this.textBoxSendee.Clear();
}
//C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针
delegate void VoidString(string s);
//C#中禁止跨线程直接访问控件
//InvokeRequired是为了解决这个问题而产生的,当一个控件的InvokeRequired属性值为真时,说明有一个创建它以外的线程想访问它。
public void Println(string s)
{
if (this.textBoxMsg.InvokeRequired) {
//如果InvokeRequired==true表示其它线程需要访问控件,那么调用invoke来转给控件owner处理。
VoidString println = Println;//此时的println和Println等同
//使用委托跨线程访问,这个事情必须自己接受其他委托做,不能其他线程直接做。
this.textBoxMsg.Invoke(println, s);
}
else {
this.textBoxMsg.AppendText(s + Environment.NewLine);
//this.textBoxMsg1.
}
}
public delegate DialogResult InvokeDelegate(Form parent);
public DialogResult XShowDialog(Form parent)
{
if (parent.InvokeRequired)
{
InvokeDelegate xShow = new InvokeDelegate(XShowDialog);
parent.Invoke(xShow, new object[] { parent });
return DialogResult;
}
return this.ShowDialog(parent);
}
public void ComboBoxAddItem(string s)
{
//comboBoxAllClients下拉列表框的集合
if (this.comboBoxAllClients.InvokeRequired) {
VoidString cbAddItem = ComboBoxAddItem;
this.textBoxMsg.Invoke(cbAddItem, s);
}
else {
this.comboBoxAllClients.Items.Add(s);
}
}
public void ComboBoxRemoveItem(string s)
{
if (this.comboBoxAllClients.InvokeRequired) {
VoidString cbRmItem = ComboBoxRemoveItem;
this.textBoxMsg.Invoke(cbRmItem, s);
}
else {
this.comboBoxAllClients.Items.Remove(s);
}
}
public string GetComboBoxItem()
{
if (this.comboBoxAllClients.SelectedItem == null)
return null;
else
return this.comboBoxAllClients.SelectedItem.ToString();
}
private void MainForm_Load(object sender, EventArgs e)
{
this.buttonListen.PerformClick();
}
}
}
其实一看Program.cs就能明白了。这里编码都不太难。
下面就是部署到服务器了。把写好的代码编译成.exe文件,放到服务器上。因为.exe只能在Windows上运行,所以为了在Linux环境下运行,需要在Linux上下载mono.下载好mono之后就直接 mono xxx.exe这样服务端就在服务器上运行起来了,再在本机打开客户端就可以通信啦。