目录
基本概念
装箱和拆箱
1、装箱拆箱的“箱”是什么,“箱”存放在哪里?
2、装箱快还是拆箱快?
3、装箱和拆箱有什么性能影响?
值类型和引用类型分别是哪些
访问权限修饰符
委托(delegate)
什么是委托链
委托链用途
事件(event)是委托吗
虚函数(virtual/override)
构造函数、析构函数可以写成虚函数么?
抽象函数(abstract)
集合
泛型
反射
多线程
常用关键字(Thread、Task、ThreadPool)
什么时候会用到多线程
进程和线程的区别
什么是死锁
死锁的四个条件
怎么避免死锁
多线程中最少要多少个资源才能保证不发生死锁?
内存
程序内存的构成
堆栈的区别
设计模式
单例模式
套字节(Socket)
用法示例
TCP和UDP区别
TCP三次握手四次挥手
基本概念
装箱和拆箱
装箱的过程,是将 值类型 转换为 引用类型 的过程; 拆箱则是将引用类型转换为值类型。
int val = 100;
object obj = val; //装箱
int num = (int) obj; //拆箱
装箱拆箱进阶
1、装箱拆箱的“箱”是什么,“箱”存放在哪里?
装箱(Boxing)操作会创建一个堆上的对象,而拆箱(Unboxing)操作则会从堆上的对象中提取值。
"箱"代表的是创建的堆上对象,它是一个将值类型包装为引用类型的容器。当进行装箱操作时,CLR会在堆上分配内存以存储值类型的值,并返回对该对象的引用。这个对象实际上是值类型的副本,被封装在引用类型内。
具体来说,当进行装箱操作时,CLR执行以下步骤:
- 在堆上分配内存,以存储值类型的值;
- 将值类型的值复制到刚分配的内存中;
- 返回对这个堆上对象的引用;
所以,装箱后的对象存放在堆上。
当进行拆箱操作时,CLR会将装箱后的对象中存储的值类型的值提取出来,并将其存储在相应的值类型变量中。
2、装箱快还是拆箱快?
一般来说,拆箱的性能要优于装箱。
装箱操作需要将值类型转换为引用类型,并在堆上创建一个对象来存储值类型的拷贝。这个过程涉及到内存分配、复制数据和类型检查,相对比较耗时。
拆箱操作则是从装箱后的对象中提取值类型的值,并将其存储在相应的值类型变量中。这个过程主要涉及到数据的复制和类型检查,相对来说相对简单和高效。
因此,一般情况下,拆箱的性能要优于装箱。频繁进行装箱操作可能会对性能产生负面影响,特别是在循环或大规模数据处理等性能敏感的场景下。所以,在需要高性能的情况下,应尽量避免不必要的装箱操作,而尽可能使用直接操作值类型的方式。
然而,需要根据具体的应用场景和代码逻辑来综合评估装箱和拆箱的性能开销。对于某些特定的情况,如需要将值类型存储在集合类(如List、ArrayList)中,不可避免地需要进行装箱操作,但要注意避免过度的装箱和拆箱操作,以提高代码的性能。
3、装箱和拆箱有什么性能影响?
装箱的性能影响:
内存分配:装箱操作需要在堆上分配额外的内存用于存储值类型的拷贝。这涉及到内存分配和释放的开销,可能导致内存碎片化。
数据复制:装箱操作会将值类型的值复制到堆上创建的对象中。这个过程涉及到数据的复制,增加了额外的时间消耗。
类型检查:CLR会进行类型检查,确保装箱后的对象是合法的引用类型。这个检查会引入附加的开销。拆箱的性能影响:
数据复制:拆箱操作会从装箱后的对象中提取值类型的值,并将其存储在相应的值类型变量中。这个过程主要涉及到数据的复制,可以说是相对较快的步骤。
类型检查:CLR会进行类型检查,确保拆箱的目标类型与装箱对象的类型匹配。这个检查会引入一定的开销。
总体而言,装箱和拆箱操作都会涉及到数据的复制和类型检查,这些操作都需要耗费额外的时间和内存。在频繁使用和大规模数据处理的场景下,过多的装箱和拆箱操作可能会降低性能并增加资源消耗。为了提高性能,应该尽量避免不必要的装箱和拆箱操作,特别是在循环或性能敏感的代码中。可以通过使用泛型集合(如List<T>)来替代装箱的集合类,或者使用值类型数组等直接操作值类型的方式,从而避免装箱和拆箱带来的性能损失。
值类型和引用类型分别是哪些
值类型:基本数据类型(int、long、short、byte、float、double、char、bool)、枚举(enum)、结构体(struct)、可空类型(在后面加问号,如:int?、double?)
引用类型:class、interface、delegate、array、string以及装箱后的可空类型
访问权限修饰符
private | 私有成员, 在类的内部才可以访问(只能从其声明上下文中进行访问) |
protected | 保护成员,该类内部和从该类派生的类中可以访问 |
protected internal | 在派生类或同一程序集内都可以访问。 |
public | 公共成员,完全公开,没有访问限制。 |
internal | 在同一程序集(dll 或 exe)中可以访问。 |
委托(delegate)
委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。说白了就是类似指向函数方法的指针。
什么是委托链
委托本身不是链表。它们只是一个存储方法地址的变量。你可以将委托看作是一个方法的引用,就像一个指针。
然而,委托可以用来实现链表的概念——委托链,它是一组委托对象,可以在这组委托中添加、删除和执行委托。
示例:
public delegate void MyDelegate();class Program {static void Main(){MyDelegate myDelegate = Method1;//用 += 运算符添加委托会创建一个委托链。移除则使用-=myDelegate += Method2;myDelegate += Method3;myDelegate();//输出:Method1 Method2 Method3Console.ReadKey();}static void Method1(){Console.Write("Method1");}static void Method2(){Console.Write("Method2");}static void Method3(){Console.Write("Method3");} }
委托链用途
委托链非常有用,可以以简单优美的方式表示程序控制流。例如,我们可以使用委托链在事件的触发和处理中实现松耦合。使用场景例如:事件处理、拦截器、插件系统等。
事件(event)是委托吗
不是委托,但它们之间有紧密的联系。事件基于委托,为委托提供了一个发布/订阅机制。事件是一种特殊的委托,它用于实现观察者模式,允许对象在特定事件发生时通知其他对象。事件的声明使用event关键词,它的返回类型是一个委托类型。在编码中尽量使用规范命名,通常以名字+Event作为事件的名称。
虚函数(virtual/override)
为了指明某个成员函数具有多态性,用关键字virtual来标记其为虚函数,表示可以被派生类重写。关键字override实现。
详细实现示例:
class A
{public virtual void Func()//注意virtual关键字,表明这是一个虚函数{Console.WriteLine("A");}
}class B:A // 注意B是从A类继承,所以A是父类,B是子类
{public override void Func() // 注意override关键字,表明重新实现了虚函数{Console.WriteLine("B");}
}class C : B // 注意C是从B类继承,所以B是父类,C是子类
{
}class D : A // 注意D是从A类继承,所以A是父类,D是子类
{public new void Func() // 注意new ,表明覆盖父类里的同名类,而不是重新实现{Console.WriteLine("D");}
}
class Program
{static void Main(string[] args){A a = new A();//A为声明类,4为实例类//A为声明类,B为实例类//A为声明类,C为实例类A b = new B();A c = new C(); A d= new D(); //A为声明类,D为实例类D d1 = new D(); //D为声明类,D为实例类a.Func(); // 执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类A,就为本身 4.执行实例类A中的方法 5.输出结果 Ab.Func();// 执行过程: 1.先检查声明类A 2.检查到是虚方法 3.转去检查实例类B,有重载的 4.执行实例类B中的方法 5.输出结果 Bc.Func();// 执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类C,无重载的 4.转去检查类C的父类B,有重载的 5.执行父类B中的方法 6.输出结果 B d.Func(); //执行过程: 1.先检查声明类 2.检查到是虚方法 3.转去检查实例类D,无重载的(注意,虽然D里有实现Func,但没有使用override关键字 ,所以不会被认为是重载) 4.转去检查类D的父类,就为本身 5.执行父类中的方法 5.输出结果 Ad1.Func(); // 执行D类里的Fun,输出结果DConsole.ReadLine();}
}
构造函数、析构函数可以写成虚函数么?
构造函数:不行
原因:构造函数是在创建对象时调用的特殊方法,用于初始化对象的状态。由于构造函数是在对象创建时立即调用的,因此它们不能被声明为虚函数。
虚函数是可以在派生类中被覆盖的函数,它们可以在运行时根据对象的实际类型动态地调用。然而,构造函数是在对象创建时立即调用的,因此它们不能被覆盖或重写。
如果需要在派生类中初始化对象的状态,可以在派生类的构造函数中调用基类的构造函数,或者在派生类中添加新的构造函数。
析构函数:不行
原因:析构函数是用于释放对象所占用的资源的方法,它们是在对象被垃圾回收器回收之前调用的。由于析构函数是在对象生命周期结束时调用的,它们也不能被声明为虚函数。
虚函数主要用于实现多态性,即在运行时根据对象的实际类型动态地调用不同的方法。而析构函数和构造函数是用于初始化或释放对象的状态,它们是在对象创建或销毁时立即调用的,因此不能被声明为虚函数。
如果需要在派生类中释放对象所占用的资源,可以在派生类的析构函数中调用基类的析构函数,或者在派生类中添加新的析构函数。
(在C++中析构函数可以写出虚函数,详情:C++构造函数、析构函数可以写成虚函数么?)
抽象函数(abstract)
抽象函数abstract修饰,是指在抽象类(Abstract Class)或接口(Interface)中声明的、没有具体实现的函数。并且必须在派生类中被重写(override)。
public abstract class A{public abstract void Func();}public class B:A{public override void Func(){throw new NotImplementedException();}}
集合
class 集合{//https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/collections //集合讲解//https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,cf7f4095e4de7646 List源码public static void Collections_Test(){//System.Collections.Generic List<string> list;//表示可按索引访问的对象的列表。 提供用于对列表进行搜索、排序和修改的方法。默认大小4,容量不够则2倍扩容Dictionary<string, string> dictionary;//表示基于键进行组织的键/ 值对的集合。SortedList<string, string> sortedList; //表示基于相关的 IComparer<T> 实现按键进行排序的键/ 值对的集合。HashSet<int> hashSet = new HashSet<int>();//值不能重复且没有顺序Queue<string> queue = new Queue<string>(); //表示对象的先进先出(FIFO) 集合。Stack<string> stack;//表示对象的后进先出(LIFO) 集合。queue.Dequeue();//尽量不用System.Collections下面的,是类型不安全的 ,性能较低//因为上面泛型中指定了类型, 是类型安全的,所以其不需要进行装箱拆箱操作,效率也就相对提升//System.CollectionsArrayList a1 = new ArrayList();// 表示对象的数组,这些对象的大小会根据需要动态增加。Hashtable b1 = new Hashtable(); b1.Add("key", "value");// 表示根据键的哈希代码进行组织的键/ 值对的集合。SortedList so1 = new SortedList(); // 表示键/值对的集合,这些键值对按键排序并可按照键和索引访问。Queue c1 = new Queue();//表示对象的先进先出(FIFO) 集合。Stack d1 = new Stack();//表示对象的后进先出(LIFO) 集合。}}
泛型
泛型约束,可用作在类或者方法上面。
where T : struct
类型参数必须是不可为 null 的值类型。 有关可为 null 的值类型的信息,请参阅可为 null 的值类型。 由于所有值类型都具有可访问的无参数构造函数,因此 struct
约束表示new()
约束,并且不能与new()
约束结合使用。struct
约束也不能与unmanaged
约束结合使用。where T : class
类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。 在可为 null 的上下文中, T
必须是不可为 null 的引用类型。where T : class?
类型参数必须是可为 null 或不可为 null 的引用类型。 此约束还应用于任何类、接口、委托或数组类型。 where T : notnull
类型参数必须是不可为 null 的类型。 参数可以是不可为 null 的引用类型,也可以是不可为 null 的值类型。 where T : default
重写方法或提供显式接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。 default
约束表示基方法,但不包含class
或struct
约束。 有关详细信息,请参阅default约束规范建议。where T : unmanaged
类型参数必须是不可为 null 的非托管类型。 unmanaged
约束表示struct
约束,且不能与struct
约束或new()
约束结合使用。where T : new()
类型参数必须具有公共无参数构造函数。 与其他约束一起使用时, new()
约束必须最后指定。new()
约束不能与struct
和unmanaged
约束结合使用。where T :
<基类名>类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中, T
必须是从指定基类派生的不可为 null 的引用类型。where T :
<基类名>?类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中, T
可以是从指定基类派生的可为 null 或不可为 null 的类型。where T :
<接口名称>类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在的可为 null 的上下文中, T
必须是实现指定接口的不可为 null 的类型。where T :
<接口名称>?类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在可为 null 的上下文中, T
可以是可为 null 的引用类型、不可为 null 的引用类型或值类型。T
不能是可为 null 的值类型。where T : U
为 T
提供的类型参数必须是为U
提供的参数或派生自为U
提供的参数。 在可为 null 的上下文中,如果U
是不可为 null 的引用类型,T
必须是不可为 null 的引用类型。 如果U
是可为 null 的引用类型,则T
可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。
反射
C#编写的程序会编译成一个程序集(.DLL或.exe),其中会包含元数据、编译代码和资源,通过反射可以获取到程序集中的信息。
反射加载dll,读取module、类、方法、特性。通俗来讲,反射就是我们在只知道一个对象的外部而不了解内部结构的情况下,可以知道这个对象的内部实现。详情请看反射详解
补充:
反射的使用场景:
假设有多种相机的SDK, 反射出DLL的成员方法,设计合理的情况下使得可以在不改程序任何代码的情况下,适应新相机的DLL。
多线程
常用关键字(Thread、Task、ThreadPool)
用法示例及解释:
class 多线程{/* * Task和Thread有区别吗?* Task和Thread都能创建⽤多线程的⽅式执⾏代码,但它们有较⼤的区别。* Task较新,发布于.NET 4.5,能结合新的async/await代码模型写代码,它不⽌能创建新线程,还能使⽤线程池(默认)、单线程等⽅式编程,* 在UI编程领域,Task还能⾃动返回UI线程上下⽂,还提供了许多便利API以管理多个Task。*/public void Thread_Test(){//Thread 的几种初始化方式Thread thread = new Thread(() => { });thread = new Thread(delegate () { });thread = new Thread(run);thread = new Thread(new ThreadStart(run));int i = 0;thread = new Thread(run2);//thread.Start(i);//传参//Task net4.5才加入进来,可以使用async, wait进行异步编程Task task = Task.Factory.StartNew(()=> { });task = new Task(()=> { });task = new Task(run);//ThreadPool 常用于解决并发问题/** 许多应用程序创建大量处于睡眠状态,等待事件发生的线程。还有许多线程可能会进入休眠状态,* 这些线程只是为了定期唤醒以轮询更改或更新的状态信息。线程池,使您可以通过由系统管理的工作线程池来更有效地使用线程。*/int workerThreads, completionPortThreads;ThreadPool.SetMaxThreads(16, 16);// 设置线程池最大线程数量ThreadPool.SetMinThreads(8, 8);ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);// 获取线程池最大线程数量ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);ThreadPool.QueueUserWorkItem(new WaitCallback(run2));}public void run(){}public void run2(object obj)//必须为object{}}
什么时候会用到多线程
并发处理:需要同时处理多个任务或请求时,可以使用多线程来并发执行不同的任务,从而提高程序的并发性和响应性。例如,Web服务器需要同时处理多个用户请求,可以使用多线程来并发处理这些请求。
资源共享:需要多个线程共享某些资源(如内存、文件等)时,可以使用多线程来协调访问和修改这些资源。例如,一个多线程的文件下载器需要同时下载多个文件,可以使用多线程来并发下载这些文件。
可扩展性:需要根据系统负载自动调整线程数量时,可以使用多线程来动态创建或销毁线程。例如,一个Web服务器需要根据用户请求数量自动调整线程池大小,可以使用多线程来实现。
提高效率:需要利用多核处理器的并行计算能力时,可以使用多线程来并行执行计算密集型任务。例如,一个图像处理软件需要对多张图片进行处理,可以使用多线程来并行处理这些图片。
进程和线程的区别
(1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
(2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
(3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
(4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
什么是死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。
死锁的四个条件
互斥条件:至少有一个资源必须处于非共享(占用)状态,即一次只能被一个进程或线程占用。
请求与保持条件:进程或线程至少需要持有一个资源,并且在等待其他资源时不释放已占有的资源。
不剥夺条件:已分配给进程或线程的资源不能被强制性地剥夺,只能由持有资源的进程或线程主动释放。
循环等待条件:存在一个进程或线程的资源申请序列,使得每个进程或线程都在等待下一个进程或线程所持有的资源。
怎么避免死锁
打破4个条件其一,
1. 破坏互斥条件:对于某些资源,可以允许多个进程或线程同时访问,从而避免资源互斥。
2. 破坏请求与保持条件:进程或线程在申请资源时,一次性申请所有需要的资源,如果无法满足,则释放已占有的资源,等待重新申请。
3. 破坏不可剥夺条件:当一个进程或线程占有一些资源时,如果申请新的资源被拒绝,可以强制性地剥夺已占有的资源,以满足其他进程或线程的需求。
4. 破坏循环等待条件:对系统中的资源进行编号,规定进程或线程只能按照编号递增的顺序申请资源,从而避免循环等待。
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测(银行家算法)
多线程中最少要多少个资源才能保证不发生死锁?
k * ( n - 1 ) + 1(k=线程,n=所需资源数)
内存
程序内存的构成
1、栈区(stack)基本数据都放这
2、堆区(heap) new出来的都放这
3、静态数据区(全局)静态,全局对象都放这
4、程序代码区
堆栈的区别
• 堆是动态分配的内存区域,用于存储程序运行时动态创建的对象和数据。
• 堆的大小通常比栈大,并且可以根据程序的需要进行动态调整。
• 对象在堆上进行分配和释放内存,需要手动管理,程序员负责手动分配和释放内存空间。
• 堆上的对象在程序的任意位置都可以被访问。
• 可以通过指针或引用来访问堆上的对象。
• 在堆上分配的内存需要手动释放,否则可能会导致内存泄漏。
• 栈是一种特殊的数据结构,用于存储函数调用时的局部变量、函数参数和返回地址等信息。
• 栈的大小通常比堆小,大小固定。
• 栈内存由编译器自动分配和释放,无需手动管理。
• 栈上的数据是按照"先进后出"(LIFO)的顺序进行存储和访问。
• 函数调用时,当前函数的状态(包括局部变量值、返回地址等)被压入栈中,当函数执行完毕时,栈顶的数据被弹出,恢复上一个函数的执行状态。
• 栈内存的分配和释放速度较快。
总结: 堆和栈是计算机内存中的两个不同区域,用于存储不同类型的数据。堆用于存储动态分配的对象和数据,大小动态调整;栈用于存储函数调用时的局部变量和函数执行状态,大小固定。堆上的内存需要手动分配和释放,而栈内存由编译器自动管理。
设计模式
单例模式
懒汉模式(下面的示例是最优写法)——顾名思义就是懒,没有对象需要调用它的时候不去实例化,有人来向它要对象的时候再实例化对象,因为懒,比我还懒
using System;
using System.Collections.Generic;
/// <summary>
/// 适用于在多线程的情况下保证只有一个实例化对象的情况,例如银行的操作系统
/// </summary>
namespace DoubleLockInstance
{//----------------------------------// 双重锁定单例public sealed class Singleton{// 定义一个类对象,用于内部实现private static Singleton myInstance;// readonly - 这个成员只能在“类初始化”时赋值 ,所谓的类初始化,就是直接在类里面初始化// 变量标记为 readonly,第一次引用类的成员时创建实例private static readonly object lockRoot = new object ();// 设置构造方法为私有,这样就不能在外部实例化类对象了private Singleton (){}// 实例化对象的方法public static Singleton GetInstance (){// 外部不能实例化对象,但是能调用类里面的静态方法// 外部需要调用这个方法来使用类对象,如果对象不存在就创建// 这里面使用两个判断是否为null的原因是,我们不需要每次都对实例化的语句进行加锁,只有当对象不存在的时候加锁就可以了if (myInstance == null) {// 锁定的作用就是为了保证当多线程同时执行这句代码的时候保证对象的唯一性// 锁定会让同时执行这段代码的线程排队执行// lock里面需要用一个已经存在的对象来判断,所以不能使用myInstancelock (lockRoot) {// 这里还需要一个判断的原因是,如果多线程都通过了外层的判断进行排队// 那将会实例化多个对象出来,所以这里还需要进行一次判断,保证线程的安全if (myInstance == null) {myInstance = new Singleton ();}}}return myInstance;}}
}
套字节(Socket)
用法示例
class SOCKET{public SOCKET(){}public void ClientSocket(){// 创建一个Socket实例Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 连接服务器IPAddress serverIP = IPAddress.Parse("127.0.0.1");IPEndPoint serverEP = new IPEndPoint(serverIP, 8888);clientSocket.Connect(serverEP);// 发送和接收数据byte[] sendData = Encoding.ASCII.GetBytes("Hello, server!");clientSocket.Send(sendData);byte[] recvData = new byte[1024];int recvLen = clientSocket.Receive(recvData);string recvMsg = Encoding.ASCII.GetString(recvData, 0, recvLen);Console.WriteLine("Received from server: " + recvMsg);//短链接的话,使用完之后,直接用下面的代码关闭就行了//长链接的话,就在你想断开的时候使用下面的代码// 关闭连接clientSocket.Shutdown(SocketShutdown.Both);clientSocket.Close();}public void StartSocketServer(){new Thread(() =>{// 创建一个Socket实例Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 绑定服务器地址和端口IPAddress serverIP = IPAddress.Parse("127.0.0.1"); // 使用任意可用的IP地址IPEndPoint serverEP = new IPEndPoint(serverIP, 8888);serverSocket.Bind(serverEP);// 监听连接请求,设置最大允许的连接数为10serverSocket.Listen(10);Console.WriteLine("等待客户端连接...");while (true){// 等待客户端连接Socket clientSocket = serverSocket.Accept();Console.WriteLine("客户端已连接: " + clientSocket.RemoteEndPoint);// 开启一个新线程处理客户端请求System.Threading.Thread clientThread = new System.Threading.Thread(() =>{try{while (true){byte[] buffer = new byte[1024];int bytesRead = clientSocket.Receive(buffer); // 接收客户端数据string message = Encoding.ASCII.GetString(buffer, 0, bytesRead);Console.WriteLine("接收到客户端数据: " + message);string response = "Hello, Client!"; // 构造响应数据clientSocket.Send(Encoding.ASCII.GetBytes(response)); // 发送响应数据}}catch (SocketException ex){Console.WriteLine("客户端断开: " + ex.Message);}catch (Exception ex){Console.WriteLine("与客户端通信发生异常: " + ex.Message);}finally{clientSocket.Shutdown(SocketShutdown.Both); // 关闭客户端连接clientSocket.Close();}});clientThread.Start();}}).Start();}#region UDPclass UDPServer{static void Main(){// 监听的 IP 地址和端口string ipAddress = "127.0.0.1";int port = 12345;// 创建 UDP 服务器套接字UdpClient udpServer = new UdpClient(port);Console.WriteLine("Server started. Waiting for clients...");while (true){// 接收客户端数据IPEndPoint clientEndPoint = new IPEndPoint(IPAddress.Any, 0);byte[] receivedBytes = udpServer.Receive(ref clientEndPoint);string request = Encoding.UTF8.GetString(receivedBytes);Console.WriteLine("Received request: {0}", request);// 做出响应string response = "Hello from server!";byte[] responseBytes = Encoding.UTF8.GetBytes(response);// 发送响应数据给客户端udpServer.Send(responseBytes, responseBytes.Length, clientEndPoint);}}}class UDPClient{static void Main(){// 服务器的 IP 地址和端口string ipAddress = "127.0.0.1";int port = 12345;// 创建 UDP 客户端套接字UdpClient udpClient = new UdpClient();// 构造服务器的终结点IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);// 发送请求数据给服务器string request = "Hello from client!";byte[] requestBytes = Encoding.UTF8.GetBytes(request);udpClient.Send(requestBytes, requestBytes.Length, serverEndPoint);// 接收服务器的响应byte[] responseBytes = udpClient.Receive(ref serverEndPoint);string response = Encoding.UTF8.GetString(responseBytes);Console.WriteLine("Received response: {0}", response);// 关闭连接udpClient.Close();}}#endregion#region TCPclass TCPServer{static void Main(){// 监听的 IP 地址和端口string ipAddress = "127.0.0.1";int port = 12345;// 创建 TCP 服务器套接字TcpListener listener = new TcpListener(IPAddress.Parse(ipAddress), port);// 开始监听客户端连接listener.Start();Console.WriteLine("Server started. Waiting for clients...");while (true){// 接受客户端连接TcpClient client = listener.AcceptTcpClient();Console.WriteLine("Client connected.");// 处理客户端请求{ // 获取网络流NetworkStream stream = client.GetStream();// 读取客户端发送的数据byte[] buffer = new byte[1024];int bytesRead = stream.Read(buffer, 0, buffer.Length);string request = Encoding.UTF8.GetString(buffer, 0, bytesRead);Console.WriteLine("Received request: {0}", request);// 做出响应string response = "Hello from server!";byte[] responseBuffer = Encoding.UTF8.GetBytes(response);// 发送响应数据给客户端stream.Write(responseBuffer, 0, responseBuffer.Length);// 关闭连接client.Close();Console.WriteLine("Client disconnected.");}}}}class TCPClient{static void Main(){// 服务器的 IP 地址和端口string ipAddress = "127.0.0.1";int port = 12345;// 创建 TCP 客户端套接字TcpClient client = new TcpClient();// 连接到服务器client.Connect(IPAddress.Parse(ipAddress), port);Console.WriteLine("Connected to server.");// 获取网络流NetworkStream stream = client.GetStream();// 发送请求数据给服务器string request = "Hello from client!";byte[] requestBuffer = Encoding.UTF8.GetBytes(request);stream.Write(requestBuffer, 0, requestBuffer.Length);// 接收服务器的响应byte[] buffer = new byte[1024];int bytesRead = stream.Read(buffer, 0, buffer.Length);string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);Console.WriteLine("Received response: {0}", response);// 关闭连接client.Close();Console.WriteLine("Disconnected from server.");}}#endregion}
TCP和UDP区别
连接性:TCP是面向连接的协议,需要在发送和接收数据前建立可靠的连接。而UDP是无连接的协议,发送数据前不需要建立连接。
可靠性:TCP提供可靠的服务,通过TCP连接传送的数据无差错,不丢失,不重复,且按序到达。而UDP尽最大努力交付,不保证可靠交付。
资源需求:TCP要求系统资源较多,而UDP较少。
传输方式:TCP是流式传输,没有边界,但保证顺序和可靠。而UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
拥塞控制:TCP有拥塞控制
TCP三次握手四次挥手
三次握手:
第一次握手(SYN):客户端向服务器发送一个连接请求报文段,报文段中包含SYN(同步序列号)标志位,并随机选择一个初始序列号(ISN)。
第二次握手(SYN/ACK):服务器接收到客户端的连接请求报文段后,向客户端发送确认报文段,报文段中包含SYN(同步序列号)和ACK(确认序列号)标志位,确认号为客户端的初始序列号+1,同时服务器也随机选择一个初始序列号。
第三次握手(ACK):客户端接收到服务器的确认报文段后,向服务器发送确认报文段,报文段中包含ACK(确认序列号)标志位和确认号,确认号为服务器的初始序列号+1。
TCP四次挥手:
第一次挥手:客户端发送一个FIN报文段,用来关闭客户端到服务器的数据传送,客户端进入FIN_WAIT_1状态。
第二次挥手:服务器收到FIN报文段后,回复一个应答报文段(ACK),表示接受关闭请求。服务器进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
第三次挥手:服务器发送一个FIN报文段,请求关闭连接。之后服务器进入LAST_ACK状态。
第四次挥手:客户端收到FIN报文段后,回复一个应答报文段(ACK),然后进入TIME_WAIT状态。服务器收到ACK报文段后,就关闭连接。
最终客户端在等待2MSL之后进入CLOSED状态。(RFC文档中规定为2分钟,但是实际实现过程中,MSL一般为:30秒、1分钟、2分钟)。