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);