还记得刚刚开始接触编程开发时,傻傻的将网站开发和网络编程混为一谈,常常因分不清楚而引为笑柄。后来勉强分清楚,又因为各种各样的协议端口之类的名词而倍感神秘,所以为了揭开网络编程的神秘面纱,本文尝试以一个简单的小例子,简述在网络编程开发中涉及到的相关知识点,仅供学习分享使用,如有不足之处,还请指正。

概述

在TCP/IP协议族中,传输层主要包括TCP和UDP两种通信协议,它们以不同的方式实现两台主机中的不同应用程序之间的数据传输,即数据的端到端传输。由于它们的实现方式不同,因此各有一套属于自己的端口号,且相互独立。采用五元组(协议,信源机IP地址,信源应用进程端口,信宿机IP地址,信宿应用进程端口)来描述两个应用进程之间的通信关联,这也是进行网络程序设计最基本的概念。传输控制协议(Transmission Control Protocol,TCP)提供一种面向连接的、可靠的数据传输服务,保证了端到端数据传输的可靠性。

涉及知识点

本例中涉及知识点如下所示:

  1. TcpClient : TcpClient类为TCP网络服务提供客户端连接,它构建于Socket类之上,以提供较高级别的TCP服务,提供了通过网络连接、发送和接收数据的简单方法。
  2. TcpListener:构建于Socket之上,提供了更高抽象级别的TCP服务,使得程序员能更方便地编写服务器端应用程序。通常情况下,服务器端应用程序在启动时将首先绑定本地网络接口的IP地址和端口号,然后进入侦听客户请求的状态,以便于客户端应用程序提出显式请求。
  3. NetworkStream:提供网络访问的基础数据流。一旦侦听到有客户端应用程序请求连接侦听端口,服务器端应用将接受请求,并建立一个负责与客户端应用程序通信的信道。

网络聊天示意图

如下图所示:看似两个在不同网络上的人聊天,实际上都是通过服务端进行接收转发的。

TCP网络通信示意图

如下图所示:首先是服务端进行监听,当有客户端进行连接时,则建立通讯通道进行通信。

示例截图

服务端截图,如下所示:

客户端截图,如下所示:开启两个客户端,开始美猴王和二师兄的对话。

核心代码

发送信息类,如下所示:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace Common
{
/// <summary>
/// 定义一个类,所有要发送的内容,都按照这个来
/// </summary>
public class ChatMessage
{
/// <summary>
/// 头部信息
/// </summary>
public ChatHeader header { get; set; } /// <summary>
/// 信息类型,默认为文本
/// </summary>
public ChatType chatType { get; set; } /// <summary>
/// 内容信息
/// </summary>
public string info { get; set; } } /// <summary>
/// 头部信息
/// </summary>
public class ChatHeader
{
/// <summary>
/// id唯一标识
/// </summary>
public string id { get; set; } /// <summary>
/// 源:发送方
/// </summary>
public string source { get; set; } /// <summary>
/// 目标:接收方
/// </summary>
public string dest { get; set; } } /// <summary>
/// 内容标识
/// </summary>
public enum ChatMark
{
BEGIN = 0x0000,
END = 0xFFFF
} public enum ChatType {
TEXT=,
IMAGE=
}
}

打包帮助类,如下所示:所有需要发送的信息,都要进行封装,打包,编码成固定格式,方便解析。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace Common
{
/// <summary>
/// 包帮助类
/// </summary>
public class PackHelper
{
/// <summary>
/// 获取待发送的信息
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
public static byte[] GetSendMsgBytes(string text, string source, string dest)
{
ChatHeader header = new ChatHeader()
{
source = source,
dest = dest,
id = Guid.NewGuid().ToString()
};
ChatMessage msg = new ChatMessage()
{
chatType = ChatType.TEXT,
header = header,
info = text
};
string msg01 = GeneratePack<ChatMessage>(msg);
byte[] buffer = Encoding.UTF8.GetBytes(msg01);
return buffer;
} /// <summary>
/// 生成要发送的包
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static string GeneratePack<T>(T t) {
string send = SerializerHelper.JsonSerialize<T>(t);
string res = string.Format("{0}|{1}|{2}",ChatMark.BEGIN.ToString("X").PadLeft(, ''), send, ChatMark.END.ToString("X").PadLeft(, ''));
int length = res.Length; return string.Format("{0}|{1}", length.ToString().PadLeft(, ''), res);
} /// <summary>
/// 解析包
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="receive">原始接收数据包</param>
/// <returns></returns>
public static T ParsePack<T>(string msg, out string error)
{
error = string.Empty;
int len = int.Parse(msg.Substring(, ));//传输内容的长度
string msg2 = msg.Substring(msg.IndexOf("|") + );
string[] array = msg2.Split('|');
if (msg2.Length == len)
{
string receive = array[];
string begin = array[];
string end = array[];
if (begin == ChatMark.BEGIN.ToString("X").PadLeft(, '') && end == ChatMark.END.ToString("X").PadLeft(, ''))
{
T t = SerializerHelper.JsonDeserialize<T>(receive);
if (t != null)
{
return t; }
else {
error = string.Format("接收的数据有误,无法进行解析");
return default(T);
}
}
else {
error = string.Format("接收的数据格式有误,无法进行解析");
return default(T);
}
}
else {
error = string.Format("接收数据失败,长度不匹配,定义长度{0},实际长度{1}", len, msg2.Length);
return default(T);
}
}
}
}

服务端类,如下所示:服务端开启时,需要进行端口监听,等待链接。

 using Common;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks; /// <summary>
/// 描述:MeChat服务端,用于接收数据
/// </summary>
namespace MeChatServer
{
public class Program
{
/// <summary>
/// 服务端IP
/// </summary>
private static string IP; /// <summary>
/// 服务端口
/// </summary>
private static int PORT; /// <summary>
/// 服务端监听
/// </summary>
private static TcpListener tcpListener; public static void Main(string[] args)
{
//初始化信息
InitInfo();
IPAddress ipAddr = IPAddress.Parse(IP);
tcpListener = new TcpListener(ipAddr, PORT);
tcpListener.Start(); Console.WriteLine("等待连接");
tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");
//如果用户按下Esc键,则结束
while (Console.ReadKey().Key != ConsoleKey.Escape)
{
Thread.Sleep();
}
tcpListener.Stop();
} /// <summary>
/// 初始化信息
/// </summary>
private static void InitInfo() {
//初始化服务IP和端口
IP = ConfigurationManager.AppSettings["ip"];
PORT = int.Parse(ConfigurationManager.AppSettings["port"]);
//初始化数据池
PackPool.ToSendList = new List<ChatMessage>();
PackPool.HaveSendList = new List<ChatMessage>();
PackPool.obj = new object();
} /// <summary>
/// Tcp异步接收函数
/// </summary>
/// <param name="ar"></param>
public static void AsyncTcpCallback(IAsyncResult ar) {
Console.WriteLine("已经连接");
ChatLinker linker = new ChatLinker(tcpListener.EndAcceptTcpClient(ar));
linker.BeginRead();
//继续下一个连接
Console.WriteLine("等待连接");
tcpListener.BeginAcceptTcpClient(new AsyncCallback(AsyncTcpCallback), "async");
}
}
}

客户端类,如下所示:客户端主要进行数据的封装发送,接收解析等操作,并在页面关闭时,关闭连接。

 using Common;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; namespace MeChatClient
{
/// <summary>
/// 聊天页面
/// </summary>
public partial class FrmMain : Form
{
/// <summary>
/// 链接客户端
/// </summary>
private TcpClient tcpClient; /// <summary>
/// 基础访问的数据流
/// </summary>
private NetworkStream stream; /// <summary>
/// 读取的缓冲数组
/// </summary>
private byte[] bufferRead; /// <summary>
/// 昵称信息
/// </summary>
private Dictionary<string, string> dicNickInfo; public FrmMain()
{
InitializeComponent();
} private void MainForm_Load(object sender, EventArgs e)
{
//获取昵称
dicNickInfo = ChatInfo.GetNickInfo();
//设置标题
string title = string.Format(":{0}-->{1} 的对话",dicNickInfo[ChatInfo.Source], dicNickInfo[ChatInfo.Dest]);
this.Text = string.Format("{0}:{1}", this.Text, title);
//初始化客户端连接
this.tcpClient = new TcpClient(AddressFamily.InterNetwork);
bufferRead = new byte[this.tcpClient.ReceiveBufferSize];
this.tcpClient.BeginConnect(ChatInfo.IP, ChatInfo.PORT, new AsyncCallback(RequestCallback), null); } /// <summary>
/// 异步请求链接函数
/// </summary>
/// <param name="ar"></param>
private void RequestCallback(IAsyncResult ar) {
this.tcpClient.EndConnect(ar);
this.lblStatus.Text = "连接服务器成功";
//获取流
stream = this.tcpClient.GetStream();
//先发送一个连接信息
string text = CommonVar.LOGIN;
byte[] buffer = PackHelper.GetSendMsgBytes(text,ChatInfo.Source,ChatInfo.Source);
stream.BeginWrite(buffer, , buffer.Length, new AsyncCallback(WriteMessage), null);
//只有stream不为空的时候才可以读
stream.BeginRead(bufferRead, , bufferRead.Length, new AsyncCallback(ReadMessage), null);
} /// <summary>
/// 发送信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSend_Click(object sender, EventArgs e)
{
string text = this.txtMsg.Text.Trim();
if( string.IsNullOrEmpty(text)){
MessageBox.Show("要发送的信息为空");
return;
}
byte[] buffer = ChatInfo.GetSendMsgBytes(text);
stream.BeginWrite(buffer, , buffer.Length, new AsyncCallback(WriteMessage), null);
this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[ChatInfo.Source]));
this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;
this.rtAllMsg.AppendText(string.Format("\r\n{0}", text));
this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Right;
} /// <summary>
/// 异步读取信息
/// </summary>
/// <param name="ar"></param>
private void ReadMessage(IAsyncResult ar)
{
if (stream.CanRead)
{
int length = stream.EndRead(ar);
if (length >= )
{ string msg = string.Empty;
msg = string.Concat(msg, Encoding.UTF8.GetString(bufferRead, , length));
//处理接收的数据
string error = string.Empty;
ChatMessage t = PackHelper.ParsePack<ChatMessage>(msg, out error);
if (string.IsNullOrEmpty(error))
{
this.rtAllMsg.Invoke(new Action(() =>
{
this.rtAllMsg.AppendText(string.Format("\r\n[{0}]", dicNickInfo[t.header.source]));
this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;
this.rtAllMsg.AppendText(string.Format("\r\n{0}", t.info));
this.rtAllMsg.SelectionAlignment = HorizontalAlignment.Left;
this.lblStatus.Text = "接收数据成功!";
}));
}
else {
this.lblStatus.Text = "接收数据失败:"+error;
}
}
//继续读数据
stream.BeginRead(bufferRead, , bufferRead.Length, new AsyncCallback(ReadMessage), null);
}
} /// <summary>
/// 发送成功
/// </summary>
/// <param name="ar"></param>
private void WriteMessage(IAsyncResult ar)
{
this.stream.EndWrite(ar);
//发送成功
} /// <summary>
/// 页面关闭,断开连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FrmMain_FormClosing(object sender, FormClosingEventArgs e)
{
if (MessageBox.Show("正在通话中,确定要关闭吗?", "关闭", MessageBoxButtons.YesNo) == DialogResult.Yes)
{
e.Cancel = false;
string text = CommonVar.QUIT;
byte[] buffer = ChatInfo.GetSendMsgBytes(text);
stream.Write(buffer, , buffer.Length);
//发送完成后,关闭连接
this.tcpClient.Close(); }
else {
e.Cancel = true;
}
}
}
}

备注:本示例中,所有的建立连接,数据接收,发送等都是采用异步方式,防止页面卡顿。

源码下载链接

备注

每一次的努力,都是幸运的伏笔。

C# 网络编程之简易聊天示例的更多相关文章

  1. 使用Android网络编程实现简易聊天室

    在Java中我们可以利用socket编程实现聊天室,在Android中也一样,因为Android完全支持JDK本身的TCP.UDP网络通信API.我们可以使用ServerSocket.Socket来建 ...

  2. 网络编程TCP协议-聊天室

    网络编程TCP协议-聊天室(客户端与服务端的交互); <span style="font-size:18px;">1.客户端发数据到服务端.</span> ...

  3. java网络编程实现两端聊天

    网络编程的三要素: ip地址:唯一标识网络上的每一台计算机 端口号:计算机中应用的标号(代表一个应用程序),0-1024系统使用或者保留端口,有效端口0-65535(short) 通信协议:通信的规则 ...

  4. python学习(九) 网络编程学习--简易网站服务器

    python `网络编程`和其他语言都是一样的,服务器这块步骤为:`1. 创建套接字``2. 绑定地址``3. 监听该描述符的所有请求``4. 有新的请求到了调用accept处理请求` Python ...

  5. 网络编程-基于Websocket聊天室(IM)系统

    目录 一.HTML5 - Websocket协议 二.聊天室(IM)系统的设计 2.1.使用者眼中的聊天系统 2.2.开发者眼中的聊天系统 2.3.IM系统的特性 2.4.心跳机制:解决网络的不确定性 ...

  6. 【Python网络编程】多线程聊天软件程序

    课程设计的时候制作的多线程聊天软件程序 基于python3.4.3 import socket import pickle import threading import tkinter import ...

  7. 基于C语言的Socket网络编程搭建简易的Web服务器(socket实现的内部原理)

    首先编写我们服务器上需要的c文件WebServer.c 涉及到的函数API: int copy(FILE *read_f, FILE * write_f) ----- 文件内容复制的方法 int Do ...

  8. Ubuntu上Qt+Tcp网络编程之简单聊天对话框

    首先看一下实现结果: >>功能: (1)服务器和客户端之间进行聊天通信: (2)一个服务器可同时给多个客户端发送消息:(全部连接时)   也可以只给特定的客户端发送消息:(连接特定IP) ...

  9. Linux C 网络编程——多线程的聊天室实现(server端)

    server端的主要功能: 实现多用户群体聊天功能(此程序最多设定为10人.可进行更改),每一个人所发送的消息其它用户均能够收到.用户能够任意的增加或退出(推出以字符串"bye"实 ...

随机推荐

  1. 区块链学习笔记:DAY05 如何使用公有云区块链服务

    这是最后一节课了,主要讲华为云在云区块链提供的服务,如何基于华为云BCS来构建应用 先来个简单的比喻: 1.有关BaaS的范围定义 包含物理主机.虚拟主机.容器服务.区块链.智能合约和服务 2.华为云 ...

  2. 5G,仅仅是更快的网速吗?

    前不久参加了华为的Dev Summit 2020开发者大会,听到了关于5G的一些分享,刚好最近对5G有一些自己的思考,在此分享给大家. 什么是5G 在这里我不想列举各种晦涩难懂的术语,简单说来,5G就 ...

  3. linux实用小命令--查看文本内容

    一.cat命令 $ cat [参数]filename 这还有一些可以和cat命令一起用的参数,可能对你有所帮助. 1.-n 参数会所有的行加上行号: $ cat -n test1 2.这个功能在检查脚 ...

  4. luogu P3830 [SHOI2012]随机树

    输入格式 输入仅有一行,包含两个正整数 q, n,分别表示问题编号以及叶结点的个数. 输出格式 输出仅有一行,包含一个实数 d,四舍五入精确到小数点后 6 位.如果 q = 1,则 d 表示叶结点平均 ...

  5. git 使用详解(6)—— 3种撤消操作

    接下来,我们会介绍一些基本的撤消操作相关的命令.请注意,有些操作并不总是可以撤消的,所以请务必谨慎小心,一旦失误,就有可能丢失部分工作成果. 修改最后一次提交 git commit --amend 有 ...

  6. Spring Boot结和Spring Data(Ehcache缓存,Thymeleaf页面,自定义异常页面跳转,Swagger2)

    项目结构 pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns=" ...

  7. [TimLinux] JavaScript 中循环执行和定时执行

    1. 两对函数 // 循环执行 // 在每个毫秒数之后,调用函数 var timeid = window.setInterval(函数名, 毫秒数); window.clearInterval(tim ...

  8. CodeForces1000A-Light It Up

    B. Light It Up time limit per test 1 second memory limit per test 256 megabytes input standard input ...

  9. [知也无涯]GAN对人脸算法的影响

    红绣被,两两间鸳鸯.不是鸟中偏爱尔,为缘交颈睡南塘.全胜薄情郎. 看到一篇GAN对人脸图像算法的影响,决心学习一个. 人脸检测 这也是我最关注的模块.文章推荐了极小面部区域人脸识别Finding ti ...

  10. Orleans[NET Core 3.1] 学习笔记(四)( 1 )创建项目

    ClassRoom ClassRoom是一个练手demo,目的是为了能熟悉掌握Orleans的基本知识和使用方法,我会尽量在这个项目中加入更多的知识点,一边学一边练避免我看完文档就忘掉 创建项目 依旧 ...