C#网络编程(一)

本文详细介绍了C#中TCP和UDP协议的网络编程步骤,涉及TcpClient/TcpListener类的使用,以及如何在服务端和客户端进行数据的发送与接收。
摘要由CSDN通过智能技术生成

C#的TCP和UDP协议网络编程步骤跟vc++的大同小异,就不做过多的解释了。

C#TCP网络编程使用的是TcpClient类和TcpListener类,读写(发送接收)数据是通过NetworkStream类。

先来讲这两个类的一些方法特性,不了解也没关系。先记着,后面会懂的。

Connect(客户端调用)连接服务端方法,如果服务端没有开启,则会出错。Connect是不会等待的。会继续执行下去。

AcceptTcpClient方法(服务端调用),调用的话,则会处于等待状态,直到客户端调用Connect方法连接服务端。AcceptTcpClient就会结束等待。执行后面的语句。

另外NetworkStream类里的Read方法调用的话,会一直处于等待状态,直到另一端调用Write方法发送数据。才会结束等待。

而Write方法则不会等待,不管另一端是否调用Read读取接收数据。它调用后立刻返回,执行后面的语句。

这样就能保持数据发送的一致性,按固定顺序发送接收。

TCP协议

服务端编写过程:

TcpListener是给服务端来用的,用来监听端口。来看一个监听端口代码示例:

     IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
    TcpListener listener = new TcpListener(ip, 9372);//9372是端口号
     listener.Start();//开始监听

IPAddress类型用来保存IP地址,上面是通过构造函数把IP地址传进去,还可以通过下面这种方式创建IPAddress并传进IP地址

IPAddress ip = IPAddress.Parse("127.0.0.1");

 (另:127.0.0.1是本地IP地址,如果你的电脑处于局域网的话,也可以用局域网地址,如192.168。。。

如果不是处于局域网的话,可以用互联网分配的IP地址。查看IP Address可以调用cmd命令ipconfig /all。

但不管你是不是处于局域网,我这个例子填127.0.0.1IP地址都是可以的。)

服务端开始监听后,就可以调用 AcceptTcpClient方法来等待一个客户端连接了,这个方法返回一个TcpClient类对象(也就是连接上的客户端),这个对象记录着客户端的一些属性,比如客户端的IP地址,绑定的端口号。对应的语句如下:

TcpClient remoteClient = listener.AcceptTcpClient();//等待客户端连接

连接上一个客户端后,就可以用GetStream方法获取这个客户端的数据流。

NetworkStream Stream = remoteClient.GetStream();//获取数据流

然后再读取数据流中的数据:

 byte[] buffer = new byte[800];//储存数据的字节数组
 int bytesRead = Stream.Read(buffer, 0, 800);//会一直等待,直到读取数据,第二个参数表明从哪里开始读取(位置),第三个参数

表明从流中读取多少数据,返回值表明实际读取了多少字节。

客户编写过程:

先创建一个TcpClient对象,关于客户端的绑定端口,是系统来分配的。

TcpClient client=new TcpClient();

然后连接服务端:

client.Connect("localhost", 9372);//与服务器连接

localhost也是本地IP地址的意思,这里填127.0.0.1也可以,当然,打个比方,如果服务端的IP地址是202.54.68.8,那么这里就得填

202.54.68.8。而我这里服务端和客户端都在同一电脑上运行。填一样的也没关系。

接着跟服务端的一样,获取服务端的数据流:

NetworkStream Stream = client.GetStream();

接着发送数据给服务端:

    String str = "Hello Server!";
    byte[] buffer = Encoding.Unicode.GetBytes(str);
    Stream.Write(buffer, 0, buffer.Length);

因为数据发送都是以字节(byte)的形式来的。要发送字符串的话,得先用Encoding.Unicode.GetBytes将字符串转化字节流,在C#中,字符默认都是双字节(Unicode编码)来存储的,也包括字符串。这一点从char类型占两个字节大小就可以看得出来。而C语言只占一个字节。

看一个完整的例子,客户端发送一个“Hello Server”的字符串给服务端,然后服务端将其显示。

服务端代码编写:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ServerConls
{
    class Program
    {
        static void Main(string[] args)
        {
            //绑定IP,监听端口
            IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
            TcpListener listener = new TcpListener(ip, 9372);
            listener.Start();
            //等待一个客户端连接
            TcpClient remoteClient = listener.AcceptTcpClient();
            //获取数据流
            NetworkStream Stream = remoteClient.GetStream();
            //从流中读取数据
            byte []buffer = new byte[800];

             int bytesRead = Stream.Read(buffer, 0, 800);
            //转化成字符串,并输出
            String str = Encoding.Unicode.GetString(buffer, 0, bytesRead);
            Console.WriteLine(str);
        }
    }
}

客户端代码编写:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ClientConls
{
    class Program
    {
        static void Main(string[] args)
        {
          //创建TcpClient对象,并连接服务端
            TcpClient client = new TcpClient();
            client.Connect("localhost", 9372);
           //获取数据流
            NetworkStream Stream = client.GetStream();
            //发送数据给服务端
            String str = "Hello Server!";
            byte[] buffer = Encoding.Unicode.GetBytes(str);
            Stream.Write(buffer, 0, buffer.Length);
        }
    }
}

  Stream.Read如果返回0,又不抛出异常,则Stream客户端已经关闭了,调用了类似client.Close方法。所以可以Read方法的返回值来

判断一个客户端的连接是否关闭。但如果客户端是非正常关闭的,则Read方法会抛出异常。

还有client.Client.LocalEndPoint和client.Client.RemoteEndPoint储存有客户端和服务端的IP地址和端口号。

另外这里我说一下ASCII(单字节)和Unicode(双字节)的问题,在C#里默认的都是以双字符来存储字符串的,如String类型。

像这个语句:

 byte[] buffer = Encoding.Unicode.GetBytes(str);

而对应也有ASCII的:

byte[] buffer=Encoding.ASCII.GetBytes(str);

它们两者有什么区别呢,一个是把Unicode转化成字符byte数组,一个是把ASCII转化成字符byte么?结果显然不是。

因为可以看到它们的参数都是String类型,而String类型,前面已经解释过了,都是双字节的。

那么只有转化后的不同了。Unicode转化后,是以双字节的方式存储字符串到byte数组里的,也就是一个字符占两个byte。

而ASCII则是一个,这一点可以输出两者的buffer长度来证明。
那么String str = Encoding.Unicode.GetString(buffer, 0, bytesRead);也是一样。

ASCII和Unicode返回的类型都是String,它们的不同也只是怎么解释buffer的存储方式。占一个字节,还是两个字节。

另外:BitConverter.GetBytes()方法可以将其它类型如整型,浮点型转化成字节数组。

与之对应的方法是ToDouble,ToInt32等。

还有一个就是结构体转化成BYTE数组的问题,我特意从网上找来了两个方法,可以用来互相转换。

两个方法如下:

 //将结构体转化成byte数组
        public static byte[] StructToBytes(object structObj)
        {
            //得到结构体的大小
            int size=System.Runtime.InteropServices.Marshal.SizeOf(structObj);
            //创建byte数组
            byte[] bytes = new byte[size];
            //分配结构体大小的内存空间
            IntPtr structPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size);
            //将结构体复制到分配好的内存空间
            System.Runtime.InteropServices.Marshal.StructureToPtr(structObj, structPtr, false);
            //从内存空间复制到byte数组
            System.Runtime.InteropServices.Marshal.Copy(structPtr, bytes, 0, size);
            //返回byte数组
            return bytes;
        }

   //将byte数组转换为结构体
        public static object BytesToStruct(byte[] bytes, Type type)
        {
            //得到结构体大小
            int size = System.Runtime.InteropServices.Marshal.SizeOf(type);
            //分配结构体大小的内存空间
            IntPtr structPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal(size);
            //将byte数组复制到分配好的内存空间
            System.Runtime.InteropServices.Marshal.Copy(bytes, 0, structPtr, size);
            //将内存空间转化为目标结构体
            object obj=System.Runtime.InteropServices.Marshal.PtrToStructure(structPtr,type);
            //返回结构体
            return obj;
        }

使用示例:

定义结构

 struct Student
 {
 public int Age;
  public int Number;
 }

客户端:

 //发送数据给服务端
 Student stu = new Student();
 stu.Age = 20;
 stu.Number = 1001;
 byte[] buffer = StructToBytes(stu);
 //发送数据
 Stream.Write(buffer, 0, buffer.Length);

服务端:

 byte []buffer = new byte[800];
 int bytesRead = Stream.Read(buffer, 0, 800);
 //转化成字符串,并输出
 Student Stu = new Student();
 Stu = (Student)BytesToStruct(buffer,Stu.GetType());
 //输出结构体里的数据
 Console.WriteLine("Age:{0},Number:{1}",Stu.Age,Stu.Number);

 UDP协议

直接看例子吧:

服务端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;


namespace ServerConls
{
  
    class Program
    {

        static void Main(string[] args)
        {
            //绑定IP地址127.0.0.1和端口9372
            IPAddress ip = IPAddress.Parse("127.0.0.1");
            IPEndPoint iep = new IPEndPoint(ip, 9372);
            UdpClient client = new UdpClient(iep);
           
            //等待客户端发送数据
            IPEndPoint RemoteIep = new IPEndPoint(IPAddress.Any,0);
            Byte[] buffer = client.Receive(ref RemoteIep);
            //输出信息
            String msg = Encoding.Unicode.GetString(buffer);
            Console.WriteLine("从{0}发来信息:{1}", RemoteIep, msg);
            //关闭连接
            client.Close();
         
        }
    }
}

客户端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace ClientConls
{
 
    class Program
    {
     
        static void Main(string[] args)
        {
            //连接服务端
            UdpClient client = new UdpClient();
            client.Connect("127.0.0.1", 9372);
            //发送数据过去
            Byte[] buffer = Encoding.Unicode.GetBytes("Hello Server!");
            client.Send(buffer, buffer.Length);
            client.Close();
        }
    }
}

需要说明的几点:
IPAddress类的作用之前已经说明了,它是用来储存IP地址的。那么 IPEndPoint也不难理解,它是用来储存IP地址和端口号。

一个IPEndPoint对象对应着一个IP地址和一个端口号。

而UdpClient类有一个构造函数的参数类型就是IPEndPoint,看下面三句代码:

 IPAddress ip = IPAddress.Parse("127.0.0.1");
 IPEndPoint iep = new IPEndPoint(ip, 9372);
 UdpClient client = new UdpClient(iep);
传进iep给client,可以说是间接的把IP地址和端口传给了UdbClient对象。这种方式传进去的IP地址和端口,表明是要绑定的IP地址和监听的端口号

跟下面这一句是不一样,虽两者的结果都是传进一个IP地址和端口号。

UdpClient client = new UdpClient("127.0.0.1", 9372);

上面这句的意思,是传进UdpClient要连接的服务器IP地址和端口号,也就是用上面那种方式创建UdpClient对象后,就不需要调用Connect函数指明连接服务端IP地址和端口了,在构造函数里,已经调用过了。

直接可以发送数据了。

另外客户端的端口号是由系统分配的,如果要自己指定的话,则通过构造函数传进去。

如:UdpClient client=new UdpClient(2234);

接着看服务端的这两句代码:

IPEndPoint RemoteIep = new IPEndPoint(IPAddress.Any,0);
 Byte[] buffer = client.Receive(ref RemoteIep);

RemoteIep是用来接收客户端的IP地址和端口号,所以建这个IPEndPoint对象时IP地址和端口号可以随便填,不会有什么影响的。我这里填的是IPAddress.Any和0。

这样得到客户端的IPEndPoint后,又可以发送数据回去,如:

//发送数据给客户端

buffer = Encoding.Unicode.GetBytes("Hello Client!");
client.Send(buffer, buffer.Length,RemoteIep);

客户端接收代码:

 IPEndPoint ServerIep = new IPEndPoint(IPAddress.Any, 0);
 buffer = client.Receive(ref ServerIep);
 String msg = Encoding.Unicode.GetString(buffer);
 Console.WriteLine("从{0}发来信息:{1}", ServerIep, msg);

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Bczheng1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值