[C#]C#网络编程基本概念和操作2

mikel阅读(805)

C#网络编程(同步传输字符串) – Part.2

源码下载:http://www.tracefact.net/SourceCode/Network-Part1-2.rar

服务端客户端通信

在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保 存到流中,所以实际上可以在客户端与服务端之间传输任何类型的数据。对客户端来说,往流中写入数据,即为向服务器传送数据;从流中读取数据,即为从服务端 接收数据。对服务端来说,往流中写入数据,即为向客户端发送数据;从流中读取数据,即为从客户端接收数据。

同步传输字符串

我们现在考虑这样一个任务:客户端打印一串字符串,然后发往服务端,服务端先输出它,然后将它改为大写,再回发到客户端,客户端接收到以后,最后再次打印一遍它。我们将它分为两部分:1、客户端发送,服务端接收并输出;2、服务端回发,客户端接收并输出。

1.客户端发送,服务端接收并输出

1.1服务端程序

我们可以在TcpClient上调用GetStream()方法来获得连接到远程计算机的流。注意这里我用了远程这个词,当在客户端调用时,它得到连接服务端的流;当在服务端调用时,它获得连接客户端的流。接下来我们来看一下代码,我们先看服务端(注意这里没有使用do/while循环):

class Server {
    static void Main(string[] args) {
        const int BufferSize = 8192;    // 缓存大小,8192字节
                   
        Console.WriteLine("Server is running … ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);
        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening …");
       
        // 获取一个连接,中断方法
        TcpClient remoteClient = listener.AcceptTcpClient();
        // 打印连接到的客户端信息
        Console.WriteLine("Client Connected!{0} <– {1}",
            remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
        // 获得流,并写入buffer中
        NetworkStream streamToClient = remoteClient.GetStream();
        byte[] buffer = new byte[BufferSize];
        int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
        Console.WriteLine("Reading data, {0} bytes …", bytesRead);
        // 获得请求的字符串
        string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
        Console.WriteLine("Received: {0}", msg);
        // 按Q退出
    }
}

这段程序的上半部分已经很熟悉了,我就不再解释。remoteClient.GetStream()方法获取到了连接至客户端的流,然后从流中读出 数据并保存在了buffer缓存中,随后使用Encoding.Unicode.GetString()方法,从缓存中获取到了实际的字符串。最后将字符 串打印在了控制台上。这段代码有个地方需要注意:在能够读取的字符串的总字节数大于BufferSize的时候会出现字符串截断现象,因为缓存中的数目总 是有限的,而对于大对象,比如说图片或者其它文件来说,则必须采用“分次读取然后转存”这种方式,比如这样:

// 获取字符串
byte[] buffer = new byte[BufferSize];
int bytesRead;          // 读取的字节数
MemoryStream msStream = new MemoryStream();
do {
    bytesRead = streamToClient.Read(buffer, 0, BufferSize);
    msStream.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
buffer = msStream.GetBuffer();
string msg = Encoding.Unicode.GetString(buffer);

这里我没有使用这种方法,一个是因为不想关注在太多的细节上面,一个是因为对于字符串来说,8192字节已经很多了,我们通常不会传递这么多的文 本。当使用Unicode编码时,8192字节可以保存4096个汉字和英文字符。使用不同的编码方式,占用的字节数有很大的差异,在本文最后面,有一段 小程序,可以用来测试Unicode、UTF8、ASCII三种常用编码方式对字符串编码时,占用的字节数大小。

现在对客户端不做任何修改,然后运行先运行服务端,再运行客户端。结果我们会发现这样一件事:服务端再打印完“Client Connected!127.0.0.1:8500 <– 127.0.0.1:xxxxx”之后,再次被阻塞了,而没有输出“Reading data, {0} bytes …”。可见,与AcceptTcpClient()方法类似,这个Read()方法也是同步的,只有当客户端发送数据的时候,服务端才会读取数据、运行此方法,否则它便会一直等待。

1.2 客户端程序

接下来我们编写客户端向服务器发送字符串的代码,与服务端类似,它先获取连接服务器端的流,将字符串保存到buffer缓存中,再将缓存写入流,写入流这一过程,相当于将消息发往服务端。

class Client {
    static void Main(string[] args) {
        Console.WriteLine("Client Running …");
        TcpClient client;
        try {
            client = new TcpClient();
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} –> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
        string msg = "\"Welcome To TraceFact.Net\"";
        NetworkStream streamToServer = client.GetStream();
        byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
        streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
        Console.WriteLine("Sent: {0}", msg);
        // 按Q退出
    }
}

现在再次运行程序,得到的输出为:

// 服务端
Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:7847
Reading data, 52 bytes …
Received: "Welcome To TraceFact.Net"
输入"Q"键退出。
// 客户端
Client Running …
Server Connected!127.0.0.1:7847 –> 127.0.0.1:8500
Sent: "Welcome To TraceFact.Net"
输入"Q"键退出。

再继续进行之前,我们假设客户端可以发送多条消息,而服务端要不断的接收来自客户端发送的消息,但是上面的代码只能接收客户端发来的一条消息,因为 它已经输出了“输入Q键退出”,说明程序已经执行完毕,无法再进行任何动作。此时如果我们再开启一个客户端,那么出现的情况是:客户端可以与服务器建立连 接,也就是netstat-a显示为ESTABLISHED,这是操作系统所知道的;但是由于服务端的程序已经执行到了最后一步,只能输入Q键退出,无法 再采取任何的动作。

回想一个上面我们需要一个服务器对应多个客户端时,对AcceptTcpClient()方法的处理办法,将它放在了do/while循环中;类似地,当我们需要一个服务端对同一个客户端的多次请求服务时,可以将Read()方法放入到do/while循环中

现在,我们大致可以得出这样几个结论:

  • 如果不使用do/while循环,服务端只有一个listener.AcceptTcpClient()方法和一个TcpClient.GetStream().Read()方法,则服务端只能处理到同一客户端的一条请求。
  • 如果使用一个do/while循环,并将listener.AcceptTcpClient()方法和TcpClient.GetStream().Read()方法都放在这个循环以内,那么服务端将可以处理多个客户端的一条请求。
  • 如果使用一个do/while循环,并将listener.AcceptTcpClient()方法放在循环之外,将TcpClient.GetStream().Read()方法放在循环以内,那么服务端可以处理一个客户端的多条请求。
  • 如果使用两个do/while循环,对它们进行分别嵌套,那么结果是什么呢?结果并不是可以处理多个客户端的多条请求。因为里层的do /while循环总是在为一个客户端服务,因为它会中断在TcpClient.GetStream().Read()方法的位置,而无法执行完毕。即使可 以通过某种方式让里层循环退出,比如客户端往服务端发去“exit”字符串时,服务端也只能挨个对客户端提供服务。如果服务端想执行多个客户端的多个请 求,那么服务端就需要采用多线程。主线程,也就是执行外层do/while循环的线程,在收到一个TcpClient之后,必须将里层的do/while 循环交给新线程去执行,然后主线程快速地重新回到listener.AcceptTcpClient()的位置,以响应其它的客户端。

对于第四种情况,实际上是构建一个服务端更为通常的情况,所以需要专门开辟一个章节讨论,这里暂且放过。而我们上面所做的,即是列出的第一种情况,接下来我们再分别看一下第二种和第三种情况。

对于第二种情况,我们按照上面的叙述先对服务端进行一下改动:

do {
    // 获取一个连接,中断方法
    TcpClient remoteClient = listener.AcceptTcpClient();
    // 打印连接到的客户端信息
    Console.WriteLine("Client Connected!{0} <– {1}",
        remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
    // 获得流,并写入buffer中
    NetworkStream streamToClient = remoteClient.GetStream();
    byte[] buffer = new byte[BufferSize];
    int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
    Console.WriteLine("Reading data, {0} bytes …", bytesRead);
    // 获得请求的字符串
    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
    Console.WriteLine("Received: {0}", msg);
} while (true);

然后启动多个客户端,在服务端应该可以看到下面的输出(客户端没有变化):

Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:8196
Reading data, 52 bytes …
Received: "Welcome To TraceFact.Net"
Client Connected!127.0.0.1:8500 <– 127.0.0.1:8199
Reading data, 52 bytes …
Received: "Welcome To TraceFact.Net"

由第2种情况改为第3种情况,只需要将do向下挪动几行就可以了:

// 获取一个连接,中断方法
TcpClient remoteClient = listener.AcceptTcpClient();
// 打印连接到的客户端信息
Console.WriteLine("Client Connected!{0} <– {1}",
    remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
// 获得流,并写入buffer中
NetworkStream streamToClient = remoteClient.GetStream();
do {
    byte[] buffer = new byte[BufferSize];
    int bytesRead = streamToClient.Read(buffer, 0, BufferSize);
    Console.WriteLine("Reading data, {0} bytes …", bytesRead);
    // 获得请求的字符串
    string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
    Console.WriteLine("Received: {0}", msg);
} while (true);

然后我们再改动一下客户端,让它发送多个请求。当我们按下S的时候,可以输入一行字符串,然后将这行字符串发送到服务端;当我们输入X的时候则退出循环:

NetworkStream streamToServer = client.GetStream();
ConsoleKey key;
Console.WriteLine("Menu: S – Send, X – Exit");
do {
    key = Console.ReadKey(true).Key;
    if (key == ConsoleKey.S) {
        // 获取输入的字符串
        Console.Write("Input the message: ");
        string msg = Console.ReadLine();
        byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
        streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
        Console.WriteLine("Sent: {0}", msg);
    }
} while (key != ConsoleKey.X);

接下来我们先运行服务端,然后再运行客户端,输入一些字符串,来进行测试,应该能够看到下面的输出结果:

// 服务端
Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:11004
Reading data, 44 bytes …
Received: 欢迎访问我的博客:TraceFact.Net
Reading data, 14 bytes …
Received: 我们一起进步!
//客户端
Client Running …
Server Connected!127.0.0.1:11004 –> 127.0.0.1:8500
Menu: S – Send, X – Exit
Input the message: 欢迎访问我的博客:TraceFact.Net
Sent: 欢迎访问我的博客:TraceFact.Net
Input the message: 我们一起进步!
Sent: 我们一起进步!

这里还需要注意一点,当客户端在TcpClient实例上调用Close()方法,或者在流上调用Dispose()方法,服务端的 streamToClient.Read()方法会持续地返回0,但是不抛出异常,所以会产生一个无限循环;而如果直接关闭掉客户端,或者客户端执行完毕 但没有调用stream.Dispose()或者TcpClient.Close(),如果服务器端此时仍阻塞在Read()方法处,则会在服务器端抛出 异常:“远程主机强制关闭了一个现有连接”。因此,我们将服务端的streamToClient.Read()方法需要写在一个try/catch中。同 理,如果在服务端已经连接到客户端之后,服务端调用remoteClient.Close(),则客户端会得到异常“无法将数据写入传输连接: 您的主机中的软件放弃了一个已建立的连接。”;而如果服务端直接关闭程序的话,则客户端会得到异常“无法将数据写入传输连接: 远程主机强迫关闭了一个现有的连接。”。因此,它们的读写操作必须都放入到try/catch块中。

2.服务端回发,客户端接收并输出

2.2服务端程序

我们接着再进行进一步处理,服务端将收到的字符串改为大写,然后回发,客户端接收后打印。此时它们的角色和上面完全进行了一下对调:对于服务端来 说,就好像刚才的客户端一样,将字符串写入到流中;而客户端则同服务端一样,接收并打印。除此以外,我们最好对流的读写操作加上lock,现在我们直接看 代码,首先看服务端:

class Server {
    static void Main(string[] args) {
        const int BufferSize = 8192;    // 缓存大小,8192Bytes
        ConsoleKey key;
        Console.WriteLine("Server is running … ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);
        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening …");
        // 获取一个连接,同步方法,在此处中断
        TcpClient remoteClient = listener.AcceptTcpClient();
        // 打印连接到的客户端信息
        Console.WriteLine("Client Connected!{0} <– {1}",
            remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
        // 获得流
        NetworkStream streamToClient = remoteClient.GetStream();
       
        do {
            // 写入buffer中
            byte[] buffer = new byte[BufferSize];
            int bytesRead;
            try {
                lock(streamToClient){
                    bytesRead = streamToClient.Read(buffer, 0, BufferSize);
                }
                if (bytesRead == 0) throw new Exception("读取到0字节");
                Console.WriteLine("Reading data, {0} bytes …", bytesRead);
                // 获得请求的字符串
                string msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                Console.WriteLine("Received: {0}", msg);
                // 转换成大写并发送
                msg = msg.ToUpper();                   
                buffer = Encoding.Unicode.GetBytes(msg);
                lock(streamToClient){
                    streamToClient.Write(buffer, 0, buffer.Length);
                }
                Console.WriteLine("Sent: {0}", msg);
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                break;
            }                          
        } while (true);
        streamToClient.Dispose();
        remoteClient.Close();
       
        Console.WriteLine("\n\n输入\"Q\"键退出。");
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}

接下来是客户端:

class Client {
    static void Main(string[] args) {
        Console.WriteLine("Client Running …");
        TcpClient client;
        ConsoleKey key;
        const int BufferSize = 8192;
        try {
            client = new TcpClient();
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} –> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
                   
        NetworkStream streamToServer = client.GetStream();         
        Console.WriteLine("Menu: S – Send, X – Exit");
        do {
            key = Console.ReadKey(true).Key;
            if (key == ConsoleKey.S) {
                // 获取输入的字符串
                Console.Write("Input the message: ");
                string msg = Console.ReadLine();
                byte[] buffer = Encoding.Unicode.GetBytes(msg);     // 获得缓存
                try {
                    lock(streamToServer){
                        streamToServer.Write(buffer, 0, buffer.Length);     // 发往服务器
                    }
                    Console.WriteLine("Sent: {0}", msg);
                    int bytesRead;
                    buffer = new byte[BufferSize];                     
                    lock(streamToServer){
                        bytesRead = streamToServer.Read(buffer, 0, BufferSize);
                    }
                    msg = Encoding.Unicode.GetString(buffer, 0, bytesRead);
                    Console.WriteLine("Received: {0}", msg);
                } catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                    break;
                }
            }
        } while (key != ConsoleKey.X);
        streamToServer.Dispose();
        client.Close();
        Console.WriteLine("\n\n输入\"Q\"键退出。");
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}

最后我们运行程序,然后输入一串英文字符串,然后看一下输出:

// 客户端
Client is running …
Server Connected!127.0.0.1:12662 –> 127.0.0.1:8500
Menu: S – Send, X – Exit
Input the message: Hello, I'm jimmy zhang.
Sent: Hello, I'm jimmy zhang.
Received: HELLO, I'M JIMMY ZHANG.
// 服务端
Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:12662
Reading data, 46 bytes …
Received: Hello, I'm jimmy zhang.
Sent: HELLO, I'M JIMMY ZHANG.

看到这里,我想你应该对使用TcpClient和TcpListener进行C#网络编程有了一个初步的认识,可以说是刚刚入门了,后面的路还很 长。本章的所有操作都是同步操作,像上面的代码也只是作为一个入门的范例,实际当中,一个服务端只能为一个客户端提供服务的情况是不存在的,下面就让我们 来看看上面所说的第四种情况,如何进行异步的服务端编程。

附录:ASCII、UTF8、Uncicode编码下的中英文字符大小

private static void ShowCode() {
    string[] strArray = { "b", "abcd", "乙", "甲乙丙丁" };
    byte[] buffer;
    string mode, back;
    foreach (string str in strArray) {
        for (int i = 0; i <= 2; i++) {
            if (i == 0) {
                buffer = Encoding.ASCII.GetBytes(str);
                back = Encoding.ASCII.GetString(buffer, 0, buffer.Length);
                mode = "ASCII";
            } else if (i == 1) {
                buffer = Encoding.UTF8.GetBytes(str);
                back = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
                mode = "UTF8";
            } else {
                buffer = Encoding.Unicode.GetBytes(str);
                back = Encoding.Unicode.GetString(buffer, 0, buffer.Length);
                mode = "Unicode";
            }
            Console.WriteLine("Mode: {0}, String: {1}, Buffer.Length: {2}",
                mode, str, buffer.Length);
            Console.WriteLine("Buffer:");
            for (int j = 0; j <= buffer.Length – 1; j++) {
                Console.Write(buffer[j] + " ");
            }
            Console.WriteLine("\nRetrived: {0}\n", back);
        }
    }
}

输出为:

Mode: ASCII, String: b, Buffer.Length: 1
Buffer: 98
Retrived: b
Mode: UTF8, String: b, Buffer.Length: 1
Buffer: 98
Retrived: b
Mode: Unicode, String: b, Buffer.Length: 2
Buffer: 98 0
Retrived: b
Mode: ASCII, String: abcd, Buffer.Length: 4
Buffer: 97 98 99 100
Retrived: abcd
Mode: UTF8, String: abcd, Buffer.Length: 4
Buffer: 97 98 99 100
Retrived: abcd
Mode: Unicode, String: abcd, Buffer.Length: 8
Buffer: 97 0 98 0 99 0 100 0
Retrived: abcd
Mode: ASCII, String: 乙, Buffer.Length: 1
Buffer: 63
Retrived: ?
Mode: UTF8, String: 乙, Buffer.Length: 3
Buffer: 228 185 153
Retrived: 乙
Mode: Unicode, String: 乙, Buffer.Length: 2
Buffer: 89 78
Retrived: 乙
Mode: ASCII, String: 甲乙丙丁, Buffer.Length: 4
Buffer: 63 63 63 63
Retrived: ????
Mode: UTF8, String: 甲乙丙丁, Buffer.Length: 12
Buffer: 231 148 178 228 185 153 228 184 153 228 184 129
Retrived: 甲乙丙丁
Mode: Unicode, String: 甲乙丙丁, Buffer.Length: 8
Buffer: 50 117 89 78 25 78 1 78
Retrived: 甲乙丙丁

大体上可以得出这么几个结论:

  • ASCII不能保存中文(貌似谁都知道=_-`)。
  • UTF8是变长编码。在对ASCII字符编码时,UTF更省空间,只占1个字节,与ASCII编码方式和长度相同;Unicode在对ASCII字符编码时,占用2个字节,且第2个字节补零。
  • UTF8在对中文编码时需要占用3个字节;Unicode对中文编码则只需要2个字节。

[C#]C#网络编程基本概念和操作

mikel阅读(903)

C#网络编程(基本概念和操作) – Part.1

源码下载:http://www.tracefact.net/SourceCode/Network-Part1-2.rar

引言

C#网络编程系列文章计划简单地讲述网络编程方面的基础知识,由于本人在这方面功力有限,所以只能提供一些初步的入门知识,希望能对刚开始学习的朋友提供一些帮助。如果想要更加深入的内容,可以参考相关书籍。

本文是该系列第一篇,主要讲述了基于套接字(Socket)进行网络编程的基本概念,其中包括TCP协议、套接字、聊天程序的三种开发模式,以及两 个基本操作:侦听端口、连接远程服务端;第二篇讲述了一个简单的范例:从客户端传输字符串到服务端,服务端接收并打印字符串,将字符串改为大写,然后再将 字符串回发到客户端,客户端最后打印传回的字符串;第三篇是第二篇的一个强化,讲述了第二篇中没有解决的一个问题,并使用了异步传输的方式来完成和第二篇 同样的功能;第四篇则演示了如何在客户端与服务端之间收发文件;第五篇实现了一个能够在线聊天并进行文件传输的聊天程序,实际上是对前面知识的一个综合应 用。

与本文相关的还有一篇文章是:C#编写简单的聊天程序,但这个聊天程序不及本系列中的聊天程序功能强大,实现方式也不相同。

网络编程基本概念

1.面向连接的传输协议:TCP

对于TCP协议我不想说太多东西,这属于大学课程,又涉及计算机科学,而我不是“学院派”,对于这部分内容,我觉得作为开发人员,只需要掌握与程序相关的概念就可以了,不需要做太艰深的研究。

我们首先知道TCP是面向连接的,它的意思是说两个远程主机(或者叫进程,因为实际上远程通信是进程之间的通信,而进程则是运行中的 程序),必须首先进行一个握手过程,确认连接成功,之后才能传输实际的数据。比如说进程A想将字符串“It's a fine day today”发给进程B,它首先要建立连接。在这一过程中,它首先需要知道进程B的位置(主机地址和端口号)。随后发送一个不包含实际数据的请求报文,我 们可以将这个报文称之为“hello”。如果进程B接收到了这个“hello”,就向进程A回复一个“hello”,进程A随后才发送实际的数据 “It's a fine day today”。

关于TCP第二个需要了解的,就是它是全双工的。意思是说如果两个主机上的进程(比如进程A、进程B),一旦建立好连接,那么数据就既可以由A流向B,也可以由B流向A。除此以外,它还是点对点的,意思是说一个TCP连接总是两者之间的,在发送中,通过一个连接将数据发给多个接收方是不可能的。TCP还有一个特性,就是称为可靠的数据传输,意思是连接建立后,数据的发送一定能够到达,并且是有序的,就是说发的时候你发了ABC,那么收的一方收到的也一定是ABC,而不会是BCA或者别的什么。

编程中与TCP相关的最重要的一个概念就是套接字。我们应该知道网络七层协议,如果我们将上面的应用程、表示层、会话层笼统地算作一 层(有的教材便是如此划分的),那么我们编写的网络应用程序就位于应用层,而大家知道TCP是属于传输层的协议,那么我们在应用层如何使用传输层的服务呢 (消息发送或者文件上传下载)?大家知道在应用程序中我们用接口来分离实现,在应用层和传输层之间,则是使用套接字来进行分离。它就像是传输层为应用层开 的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据;而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,我们 是不知道也不需要知道的,我们也不会关心它如何传输,这属于网络其它层次的工作。

举个例子,如果你想写封邮件发给远方的朋友,那么你如何写信、将信打包,属于应用层,信怎么写,怎么打包完全由我们做主;而当我们将信投入邮筒时, 邮筒的那个口就是套接字,在进入套接字之后,就是传输层、网络层等(邮局、公路交管或者航线等)其它层次的工作了。我们从来不会去关心信是如何从西安发往 北京的,我们只知道写好了投入邮筒就OK了。可以用下面这两幅图来表示它:

注意在上面图中,两个主机是对等的,但是按照约定,我们将发起请求的一方称为客户端,将另一端称为服务端。可以看出两个程序之间的对话是通过套接字这个出入口来完成的,实际上套接字包含的最重要的也就是两个信息:连接至远程的本地的端口信息(本机地址和端口号),连接到的远程的端口信息(远程地址和端口号)。注意上面词语的微妙变化,一个是本地地址,一个是远程地址。

这里又出现了了一个名词端口。一般来说我们的计算机上运行着非常多的应用程序,它们可能都需要同远程主机打交道,所以远程主机就需要 有一个ID来标识它想与本地机器上的哪个应用程序打交道,这里的ID就是端口。将端口分配给一个应用程序,那么来自这个端口的数据则总是针对这个应用程序 的。有这样一个很好的例子:可以将主机地址想象为电话号码,而将端口号想象为分机号。

在.NET中,尽管我们可以直接对套接字编程,但是.NET提供了两个类将对套接字的编程进行了一个封装,使我们的使用能够更加方便,这两个类是TcpClient和TcpListener,它与套接字的关系如下:

从上面图中可以看出TcpClient和TcpListener对套接字进行了封装。从中也可以看出,TcpListener位于接收流的位 置,TcpClient位于输出流的位置(实际上TcpListener在收到一个请求后,就创建了TcpClient,而它本身则持续处于侦听状态,收 发数据都可以由TcpClient完成。这个图有点不够准确,而我暂时没有想到更好的画法,后面看到代码时会更加清楚一些)。

我们考虑这样一种情况:两台主机,主机A和主机B,起初它们谁也不知道谁在哪儿,当它们想要进行对话时,总是需要有一方发起连接,而另一方则需要对本机的某一端口进行侦听。而在侦听方收到连接请求、并建立起连接以后,它们之间进行收发数据时,发起连接的一方并不需要再进行侦听。因为连接是全双工的,它可以使用现有的连接进行收发数据。而我们前面已经做了定义:将发起连接的一方称为客户端,另一段称为服务端,则现在可以得出:总是服务端在使用TcpListener类,因为它需要建立起一个初始的连接

2.网络聊天程序的三种模式

实现一个网络聊天程序本应是最后一篇文章的内容,也是本系列最后的一个程序,来作为一个终结。但是我想后面更多的是编码,讲述的内容应该不会太多,所以还是把讲述的东西都放到这里吧。

当采用这种模式时,即是所谓的完全点对点模式,此时每台计算机本身也是服务器,因为它需要进行端口的侦听。实现这个模式的难点是:各个主机(或终 端)之间如何知道其它主机的存在?此时通常的做法是当某一主机上线时,使用UDP协议进行一个广播(Broadcast),通过这种方式来“告知”其它主 机自己已经在线并说明位置,收到广播的主机发回一个应答,此时主机便知道其他主机的存在。这种方式我个人并不喜欢,但在 C#编写简单的聊天程序 这篇文章中,我使用了这种模式,可惜的是我没有实现广播,所以还很不完善。

第二种方式较好的解决了上面的问题,它引入了服务器,由这个服务器来专门进行广播。服务器持续保持对端口的侦听状态,每当有主机上线时,首先连接至 服务器,服务器收到连接后,将该主机的位置(地址和端口号)发往其他在线主机(绿色箭头标识)。这样其他主机便知道该主机已上线,并知道其所在位置,从而 可以进行连接和对话。在服务器进行了广播之后,因为各个主机已经知道了其他主机的位置,因此主机之间的对话就不再通过服务器(黑色箭头表示),而是直接进 行连接。因此,使用这种模式时,各个主机依然需要保持对端口的侦听。在某台主机离线时,与登录时的模式类似,服务器会收到通知,然后转告给其他的主机。

第三种模式是我觉得最简单也最实用的一种,主机的登录与离线与第二种模式相同。注意到每台主机在上线时首先就与服务器建立了连接,那么从主机A发往 主机B发送消息,就可以通过这样一条路径,主机A –> 服务器 –> 主机B,通过这种方式,各个主机不需要在对端口进行侦听,而只需要服务器进行侦听就可以了,大大地简化了开发。

而对于一些较大的文件,比如说图片或者文件,如果想由主机A发往主机B,如果通过服务器进行传输效率会比较低,此时可以临时搭建一个主机A至主机B之间的连接,用于传输大文件。当文件传输结束之后再关闭连接(桔红色箭头标识)。

除此以外,由于消息都经过服务器,所以服务器还可以缓存主机间的对话,即是说当主机A发往主机B时,如果主机B已经离线,则服务器可以对消息进行缓存,当主机B下次连接到服务器时,服务器自动将缓存的消息发给主机B。

本系列文章最后采用的即是此种模式,不过没有实现过多复杂的功能。接下来我们的理论知识告一段落,开始下一阶段――漫长的编码。

基本操作

1.服务端对端口进行侦听

接下来我们开始编写一些实际的代码,第一步就是开启对本地机器上某一端口的侦听。首先创建一个控制台应用程序,将项目名称命名为 ServerConsole,它代表我们的服务端。如果想要与外界进行通信,第一件要做的事情就是开启对端口的侦听,这就像为计算机打开了一个“门”,所 有向这个“门”发送的请求(“敲门”)都会被系统接收到。在C#中可以通过下面几个步骤完成,首先使用本机Ip地址和端口号创建一个 System.Net.Sockets.TcpListener类型的实例,然后在该实例上调用Start()方法,从而开启对指定端口的侦听。

using System.Net;               // 引入这两个命名空间,以下同
using System.Net.Sockets;
using// 略

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running … ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);
        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening …");
        Console.WriteLine("\n\n输入\"Q\"键退出。");
        ConsoleKey key;
        do {
            key = Console.ReadKey(true).Key;
        } while (key != ConsoleKey.Q);
    }
}
// 获得IPAddress对象的另外几种常用方法:
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPAddress ip = Dns.GetHostEntry("localhost").AddressList[0];   

上面的代码中,我们开启了对8500端口的侦听。在运行了上面的程序之后,然后打开“命令提示符”,输入“netstat-a”,可以看到计算机器中所有打开的端口的状态。可以从中找到8500端口,看到它的状态是LISTENING,这说明它已经开始了侦听:

  TCP    jimmy:1030             0.0.0.0:0              LISTENING
  TCP    jimmy:3603             0.0.0.0:0              LISTENING
  TCP    jimmy:8500             0.0.0.0:0              LISTENING
  TCP    jimmy:netbios-ssn     0.0.0.0:0              LISTENING

在打开了对端口的侦听以后,服务端必须通过某种方式进行阻塞(比如Console.ReadKey()),使得程序不能够因为运行结束而退出。否则 就无法使用“netstat -a”看到端口的连接状态,因为程序已经退出,连接会自然中断,再运行“netstat -a”当然就不会显示端口了。所以程序最后按“Q”退出那段代码是必要的,下面的每段程序都会含有这个代码段,但为了节省空间,我都省略掉了。

2.客户端与服务端连接

2.1单一客户端与服务端连接

当服务器开始对端口侦听之后,便可以创建客户端与它建立连接。这一步是通过在客户端创建一个TcpClient的类型实例完成。每创建一个新的 TcpClient便相当于创建了一个新的套接字Socket去与服务端通信,.Net会自动为这个套接字分配一个端口号,上面说过,TcpClient 类不过是对Socket进行了一个包装。创建TcpClient类型实例时,可以在构造函数中指定远程服务器的地址和端口号。这样在创建的同时,就会向远 程服务端发送一个连接请求(“握手”),一旦成功,则两者间的连接就建立起来了。也可以使用重载的无参数构造函数创建对象,然后再调用Connect() 方法,在Connect()方法中传入远程服务器地址和端口号,来与服务器建立连接。

这里需要注意的是,不管是使用有参数的构造函数与服务器连接,或者是通过Connect()方法与服务器建立连接,都是同步方法(或者说是阻塞的,英文叫block)。 它的意思是说,客户端在与服务端连接成功、从而方法返回,或者是服务端不存、从而抛出异常之前,是无法继续进行后继操作的。这里还有一个名为 BeginConnect()的方法,用于实施异步的连接,这样程序不会被阻塞,可以立即执行后面的操作,这是因为可能由于网络拥塞等问题,连接需要较长 时间才能完成。网络编程中有非常多的异步操作,凡事都是由简入难,关于异步操作,我们后面再讨论,现在只看同步操作。

创建一个新的控制台应用程序项目,命名为ClientConsole,它是我们的客户端,然后添加下面的代码,创建与服务器的连接:

class Client {
    static void Main(string[] args) {
        Console.WriteLine("Client Running …");
        TcpClient client = new TcpClient();
        try {
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} –> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
        // 按Q退出
    }
}

上面带代码中,我们通过调用Connect()方法来与服务端连接。随后,我们打印了这个连接消息:本机的Ip地址和端口号,以及连接到的远程Ip 地址和端口号。TcpClient的Client属性返回了一个Socket对象,它的LocalEndPoint和RemoteEndPoint属性分 别包含了本地和远程的地址信息。先运行服务端,再运行这段代码。可以看到两边的输出情况如下:

// 服务端:
Server is running …
Start Listening …
// 客户端:
Client Running …
Server Connected!127.0.0.1:4761 –> 127.0.0.1:8500

我们看到客户端使用的端口号为4761,上面已经说过,这个端口号是由.NET随机选取的,并不需要我们来设置,并且每次运行时,这个端口号都不同。再次打开“命令提示符”,输入“netstat -a”,可以看到下面的输出:

  TCP    jimmy:8500             0.0.0.0:0              LISTENING
  TCP    jimmy:8500             localhost:4761         ESTABLISHED
  TCP    jimmy:4761             localhost:8500         ESTABLISHED

从这里我们可以得出几个重要信息:1、端口8500和端口4761建立了连接,这个4761端口便是客户端用来与服务端进行通信的端口;2、 8500端口在与客户端建立起一个连接后,仍然继续保持在监听状态。这也就是说一个端口可以与多个远程端口建立通信,这是显然的,大家众所周之的HTTP 使用的默认端口为80,但是一个Web服务器要通过这个端口与多少个浏览器通信啊。

2.2多个客户端与服务端连接

那么既然一个服务器端口可以应对多个客户端连接,那么接下来我们就看一下,如何让多个客户端与服务端连接。如同我们上面所说的,一个TcpClient就是一个Socket,所以我们只要创建多个TcpClient,然后再调用Connect()方法就可以了:

class Client {
    static void Main(string[] args) {
        Console.WriteLine("Client Running …");
        TcpClient client;
        for (int i = 0; i <= 2; i++) {
            try {
                client = new TcpClient();
                client.Connect("localhost", 8500);      // 与服务器连接
            } catch (Exception ex) {
                Console.WriteLine(ex.Message);
                return;
            }
            // 打印连接到的服务端信息
            Console.WriteLine("Server Connected!{0} –> {1}",
                client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
        }                  
        // 按Q退出
    }
}

上面代码最重要的就是client = new TcpClient()这句,如果你将这个声明放到循环外面,再循环的第二趟就会发生异常,原因很显然:一个TcpClient对象对应一个Socket,一个Socket对应着一个端口,如果不使用new操作符重新创建对象,那么就相当于使用一个已经与服务端建立了连接的端口再次与远程建立连接

此时,如果在“命令提示符”运行“netstat -a”,则会看到类似下面的的输出:

  TCP    jimmy:8500             0.0.0.0:0               LISTENING
  TCP    jimmy:8500             localhost:10282        ESTABLISHED
  TCP    jimmy:8500             localhost:10283        ESTABLISHED
  TCP    jimmy:8500             localhost:10284        ESTABLISHED
  TCP    jimmy:10282            localhost:8500         ESTABLISHED
  TCP    jimmy:10283            localhost:8500         ESTABLISHED
  TCP    jimmy:10284            localhost:8500         ESTABLISHED

可以看到创建了三个连接对,并且8500端口持续保持侦听状态,从这里以及上面我们可以推断出TcpListener的Start()方法是一个异步方法。

3.服务端获取客户端连接

3.1获取单一客户端连接

上面服务端、客户端的代码已经建立起了连接,这通过使用“netstat -a”命令,从端口的状态可以看出来,但这是操作系统告诉我们的。那么我们现在需要知道的就是:服务端的程序如何知道已经与一个客户端建立起了连接?

服务器端开始侦听以后,可以在TcpListener实例上调用AcceptTcpClient()来获取与一个客户端的连接,它返回一个 TcpClient类型实例。此时它所包装的是由服务端去往客户端的Socket,而我们在客户端创建的TcpClient则是由客户端去往服务端的。这 个方法是一个同步方法(或者叫阻断方法,block method),意思就是说,当程序调用它以后,它会一直等待某个客户端连接,然后才会返回,否则就会一直等下去。这样的话,在调用它以后,除非得到一个 客户端连接,不然不会执行接下来的代码。一个很好的类比就是Console.ReadLine()方法,它读取输入在控制台中的一行字符串,如果有输入, 就继续执行下面代码;如果没有输入,就会一直等待下去。

class Server {
    static void Main(string[] args) {
        Console.WriteLine("Server is running … ");
        IPAddress ip = new IPAddress(new byte[] { 127, 0, 0, 1 });
        TcpListener listener = new TcpListener(ip, 8500);
        listener.Start();           // 开始侦听
        Console.WriteLine("Start Listening …");
        // 获取一个连接,中断方法
        TcpClient remoteClient = listener.AcceptTcpClient();
        // 打印连接到的客户端信息
        Console.WriteLine("Client Connected!{0} <– {1}",
           remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
        // 按Q退出
    }
}

运行这段代码,会发现服务端运行到listener.AcceptTcpClient()时便停止了,并不会执行下面的 Console.WriteLine()方法。为了让它继续执行下去,必须有一个客户端连接到它,所以我们现在运行客户端,与它进行连接。简单起见,我们 只在客户端开启一个端口与之连接:

class Client {
    static void Main(string[] args) {
        Console.WriteLine("Client Running …");
        TcpClient client = new TcpClient();
        try {
            client.Connect("localhost", 8500);      // 与服务器连接
        } catch (Exception ex) {
            Console.WriteLine(ex.Message);
            return;
        }
        // 打印连接到的服务端信息
        Console.WriteLine("Server Connected!{0} –> {1}",
            client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
        // 按Q退出
    }
}

此时,服务端、客户端的输出分别为:

// 服务端
Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:5188
// 客户端
Client Running …
Server Connected!127.0.0.1:5188 –> 127.0.0.1:8500

3.2获取多个客户端连接

现在我们再接着考虑,如果有多个客户端发动对服务器端的连接会怎么样,为了避免你将浏览器向上滚动,来查看上面的代码,我将它拷贝了下来,我们先看下客户端的关键代码:

TcpClient client;
for (int i = 0; i <=2; i++) {
    try {
        client = new TcpClient();
        client.Connect("localhost", 8500);      // 与服务器连接
    } catch (Exception ex) {
        Console.WriteLine(ex.Message);
        return;
    }
    // 打印连接到的服务端信息
    Console.WriteLine("Server Connected!{0} –> {1}",
        client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
}

如果服务端代码不变,我们先运行服务端,再运行客户端,那么接下来会看到这样的输出:

// 服务端
Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:5226
// 客户端
Client Running …
Server Connected!127.0.0.1:5226 –> 127.0.0.1:8500
Server Connected!127.0.0.1:5227 –> 127.0.0.1:8500
Server Connected!127.0.0.1:5228 –> 127.0.0.1:8500

就又回到了本章第2.2小节“多个客户端与服务端连接”中的处境:尽管有三个客户端连接到了服务端,但是服务端程序只接收到了一个。这是因为服务端只调用了一次listener.AcceptTcpClient(),而它只对应一个连往客户端的Socket。但是操作系统是知道连接已经建立了的,只是我们程序中没有处理到,所以我们当我们输入“netstat -a”时,仍然会看到3对连接都已经建立成功。

为了能够接收到三个客户端的连接,我们只要对服务端稍稍进行一下修改,将AcceptTcpClient方法放入一个do/while循环中就可以了:

Console.WriteLine("Start Listening …");
while (true) {
    // 获取一个连接,同步方法
    TcpClient remoteClient = listener.AcceptTcpClient();
    // 打印连接到的客户端信息
    Console.WriteLine("Client Connected!{0} <– {1}",
        remoteClient.Client.LocalEndPoint, remoteClient.Client.RemoteEndPoint);
}

这样看上去是一个死循环,但是并不会让你的机器系统资源迅速耗尽。因为前面已经说过了,AcceptTcpClient()再没有收到客户端的连接 之前,是不会继续执行的,它的大部分时间都在等待。另外,服务端几乎总是要保持在运行状态,所以这样做并无不可,还可以省去“按Q退出”那段代码。此时再 运行代码,会看到服务端可以收到3个客户端的连接了。

Server is running …
Start Listening …
Client Connected!127.0.0.1:8500 <– 127.0.0.1:5305
Client Connected!127.0.0.1:8500 <– 127.0.0.1:5306
Client Connected!127.0.0.1:8500 <– 127.0.0.1:5307

本篇文章到此就结束了,接下来一篇我们来看看如何在服务端与客户端之间收发数据。

[Java]基于socket server push技术的聊天室实现(下)

mikel阅读(782)

基于socket server push技术的聊天室实现(下)

下面我们来看看具体实现流程:
  聊天服务器的实现
  我们的服务器的核心部分是ThreadedChatHandle类,我们需要处理的数据主要包括两部分——在线列表和用户发言。在线列表可以直接使用大的对象数组,这是基于一个聊天室容量是有限制的考虑。而用户的发言直接发到管道里面就可以了。
  在线列表类的定义如下:
  class Chater
  { private static Double id;//这个ID作为区别号,同时
   private Double socketid; file://与聊天主帧对应的Socket相关联。
   private String nickname;// 用户昵称
   private String passwd;// 用户昵称
   private Int privilige;//
   private String[] filter;//某个用户的过滤列表
   private Double login_time;//记录登录时间,用以清除一些超时联接
   private String color;//用户聊天颜色
  ……//限于篇幅,省略了相关的方法。
  }
   注意:以上用户数据大部分是在login阶段,用户通过身份验证以后填入的。只有socketid要等到聊天主帧(一个普通的聊天界面包括聊天主帧,发 言帧,在线列表帧三个部分)显示以后才得到。如果超过一定时间,socketid还是没有填入,说明浏览器取得主框架以后连接中断了,这时候就需要删除该 用户数据。如果要实现象sohu那样的私聊的话,还应该增加用户IP地址的属性。
  用户发言类的定义如下:
  class Content
  { private Double timestamp;//时间戳
  private Double fromChaterid;//发言人id
  private Double toChaterid;//聊天对象id
  private Boolen isSecurity;//是否私聊标志
  private String theContent;//聊天内容,在构建器里处理过,已经包括表情等ht
ml文本。
  ……//限于篇幅,省略了相关的方法。
  }
  
   核心的ThreadedChatHandle类主要处理的工作是分析用户请求。客户端发送的请求的值,主要有login(验证身份,显示聊天室主框 架)、joinchat(初始化聊天信息,如显示欢迎等,显示聊天内容显示帧,并保持连接,发送聊天信息。)、showtalk(显示发言的帧)、 names(显示在线列表帧)、leave(用户点击按钮离开聊天室)等等。
  假如我们使用GET方法传递数据而不是通过POST方法提交表单的话,用户数据输入都是在URL里传送,下面是几个url实例,结合后面客户端流程,可以更好地理解ThreadedChatHandle类的职能:
  这是一个用户名密码均为’aaa’的聊天用户登录系统,说了一句话“hello”,然后退出所产生的一系列请求:
  /login?name=aaa&passwd=aaa
  /joinchat?chaterid=555
  /showtalk?chaterid=555
  /names?chaterid=555
  /speak?chaterid=555
  /leave?chaterid=555
  ……
  以上是服务器程序流程,实际上我们参数的传递不能只传一个 chaterid,还需要有个对应的认证。而names传递一个chaterid是为了更新时间在线列表类内自己访问的时间,避免连接超时。下面我们从客户端看看具体登录过程。
  聊天界面由三个frame组成,其中joinchat帧是聊天内容显示部分;showtalk帧是用户输入部分,包括聊天内容输入、动作、过滤以及管理功能都在这一帧输入;names是在线名单显示部分,这一部分是定时刷新的。
  让我们从浏览器的角度来看看进入聊天室的过程。
  ◆首先浏览器请求页面
  http://host:8080/login?name=NAME&passwd=PWD
  此时一个ThreadedChatHandle出现(包括了一个socket连接),并发送了一行数据:
  GET /login?name=NAME&passwd=PWD HTTP/1.1
  ◆服务器生成一个session id,验证密码以后,发回:
  HTTP/1.1 200 OK
  <其他头信息>
  Content-TYPE: text/html
  <空行>
  <html>
  ……
  <frameset cols="*,170" rows="*" border="1" framespacing="1">
   <frameset rows="*,100,0" cols="*" border="0" framespacing="0">
    <frame src="/joinchat?chaterid=555" name="u" frameborder="NO" noresize>
    <frame src="/showtalk?chaterid=555" name="d" frameborder="NO" noresize>
   </frameset>
   <frame src="/names?chaterid=555" name="r" noresize>
  </frameset>
  ……
  </html>
  然后ThreadedChatHandle.start()退出,本子线程结束
  ◆浏览器收到以上html文件后,将会依次打开三个联接(其中的chaterid是需要传递的变量,555是个虚指):
  /joinchat?chaterid=555
  /showtalk?chaterid=555
  /names?chaterid=555
   这三个联接中的第一个联接joinchat在整个聊天过程中都是保持联接的,这样从浏览器角度来看,就是一个始终下载不完的大页面,显示效果上就是聊天 内容不是靠刷新来更新,而是不断地向上滚动。通过察看html代码可以看到,只有<html><body>,然后就是不断增加的聊天内容,没有 </body></html>。
  另外两个联接在页面发送完毕以后,处理这两个连接的线程就结束了。
  这样一次登录聊天室实际上有四个子线程响应,但登录完成以后,只有处理joinchat帧的线程依然存活,用于接收来自服务器的聊天信息,这是基于推技术聊天室的关键所在。
  当然,如果用户有其它操作的请求,例如用户注册、修改昵称、修改密码等操作都可以通过类的扩充得到相对应的响应。通过对类方法的重载还可以比较方便的根据需要修改用户认证机制与网站其它功能模块结合在一块。

[Java]基于socket server push技术的聊天室实现(上)

mikel阅读(904)

基于socket server push技术的聊天室实现(上)

基于推技术的聊天室在国内现在已经比较常见。这种聊天室最大的特点是不使用浏览器每格一段时间就刷新的方式,而让服务器不定时往客户端写聊天的内容。当有 人发言时,屏幕上才会出现新聊天内容,而且聊天内容是不断向上滚动的,如果浏览器状态栏在的话,可以看到进度条始终处于下载页面状态。即使这种聊天室容纳 上百人,性能不会明显的降低。而以往的CGI或活动服务器端脚本做的聊天室性能明显就不行了。
   推技术的聊天室聊天室基本原理是,不使用HTTPD服务器程序,由自己的Socket程序监听服务器的80端口,根据html规范,在接收到浏览器的请 求以后,模仿www服务器的响应,将聊天内容发回浏览器。在浏览器看来就象浏览一个巨大的页面一样始终处于页面接收状态。也就是说,我们不再使用CGI等 方式来处理聊天的内容,而采用我们自己的程序来处理所有的事务。实际上它就是一个专门的聊天服务器,即一个简化了的专门用于聊天的WWW服务器。
  在具体讨论程序的实现之前,我们先来解析一下相关的技术。
◆http请求和应答过程
  http协议是浏览器与WWW服务器之间通信的标准,Socket聊天服务器应当遵守这个协议。实际上,我们只需要使用其中的一小部分就可以了。
   http使用了C/S(客户/服务器)模式,其中浏览器是http客户,浏览某个页面实际上就是打开一个Socket连接,发送一个请求到WWW服务 器,服务器根据所请求的资源发送应答给浏览器,然后关闭连接。客户和服务器之间的请求和应答有一定的格式要求,只要按照这个格式接收请求发送应答,浏览器 就会正常的显示你所需要的的内容。
  请求和应答具有类似的结构,包括:
  · 一个初始行
  · 0个或多个header lines
  · 一个空行
  · 可选的信息
  我们看看一个浏览器发出的请求:
  当我们浏览网页http://www.somehost.com/path/file.html的时候,浏览器首先打开一个到主机www.somehost.com的80端口的socket,然后发送以下请求:
  GET /path/file.html HTTP/1.0
  From: someuser@somehost.com
  User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0; DigExt)
  [空行]
  第一行GET /path/file.html HTTP/1.0是我们需要处理的核心。由以空格分隔的三部分组成,方法(method):GET,请求资源:/path/file.html,http版本:HTTP/1.0。
  服务器将会通过同一个socket用以下信息回应:
  HTTP/1.0 200 OK
  Date: Fri, 31 Dec 1999 23:59:59 GMT
  Content-Type: text/html
  Content-Length: 1354
  <html>
  <body>
  <h1>Hello world!</h1>
(其他内容)…
  </body>
  </html>
  第一行同样也包括三部分:http版本,状态码,与状态码相关的描述。状态码200表示请求成功。
  发送完应答信息以后,服务器就会关闭socket。
◆服务器模型
  一般网络服务器主要分为两种:
   (1)循环服务器(iterative server):它是一个时刻只能处理一个请求的服务器,多个请求同时到来将会放在请求队列里。TCP套接字服务器一般很少采用循环方式,因为假如某个客 户和服务器的连接出了问题,会导致整个服务器挂掉。它常为UDP套接字服务器所采用。
  (2)并发服务器(concurrent server):在每个请求到来以后分别产生一个新进程来处理这个请求所产生的连接。TCP的Socket服务器大多采用并发方式提供服务。
  并发服务器有多种实现方法:
  i 服务器和每个接收到的客户机进行连接,创建一个新的子进程处理这个客户机请求。
  ii 服务器预先创建多个子进程,由这个子进程处理客户机请求。这种方式被称为“预创建(prefork)”服务器。
  iii 服务器用函数select实现对多个客户机连接的多路复用。
  iv 超级服务器(inet)激活的服务器。
   并发服务器由于其算法而具有与生俱来的快速响应优势,而且当某一个用户与服务器通信死锁不会影响其他进程,但由于多个进程之间需要通过进程间通信实现信 息交换,而且fork新进程所带来的开销随着用户数量的增加越来越大,因此原始的并发服务器并不一定是最好的选择。JAVA语言给我们带来的方便的线程机 制,使我们可以用多线程来代替多进程,实现并发服务器,为我们进行快速的商业版本的聊天室的开发提供了优势。
  值得注意的是,在linux下,JAVA并没有实现真正的多线程,本质上仍然是多进程。
  ◆POST与GET
   提交form表单信息一般常用的有两种:POST或者GET。POST由于长度不受限制,而作为大多数form提交时使用的方法。GET方法通过URL 来发送提交信息,由于URL被WWW服务器限制了长度,一般最长只能为1024字节,所以如果发送信息很长的话,就不能使用这种方法。
  由于我们对聊天内容有长度限制,不会太长,而且普通浏览页面使用GET方法,使用GET方法提交form表单可以简化处理过程,所以我们可以使用这种方法来提交聊天内容。
  我们感到美中不足的是GET方法将提交的内容简单的附在连接后边,我们如果能够将提交的内容进行HTML编码的话,就可以让客户舒服点了。
 ◆用JAVA实现并发SOCKET通信
  如果以前做过C的SOCKET编程,那么这一段对你来说将不是什么难事。利用JAVA的多线程机制我们可以非常方便的实现并发服务。
  每当我们知道服务器主程序创建一个新的套接字连接(即成功地调用了accept()方法)的时候,就启动一个新的线程来负责本服务器和该客户之间的连接,主程序将返回并等待下一个连接。为了实现这个方案,本服务器主循环应该采用如下形式:
  while(true)
  { Socket newjoin=s.accept();
  Tread t=new ThreadedChatHandle(newjoin);
  t.start();
  }
   ThreadedChatHandle类是从Thread类衍生出的处理聊天过程的子类,它的run()方法包括了服务器和客户的通信循环——判断客户 的请求(例如登录、发言、刷新在线列表),处理发言数据,发送聊天信息等等。下面是一个服务器程序的例子,可以帮助初学者尽快理解。
  import java.io.*;
  import java.net.*;
  public class ChatServer
  { public static void main(String[] args)
  { int I=1;
  try
  {ServerSocket s=new ServerSocket(8080);
  /*创建一个监视8080端口的服务器套接字,如果需要,你可以改成80端口*/
  for(;;)
  { Socket newjoin=s.accept();
  /*等待一个连接。如果这个连接没有被创建,本方法阻塞当前线程。返回值是一个
Socket对象,服务器程序利用这个对象可以与连接的客户通信。*/
   System.out.println(“新连接”+i);
   new ThreadedChatHandle(newjoin,i).start();
  /* ThreadedChatHandle(Socket theS,int c)是我们自己定义的聊天服务类,这个
类在后边我们有进一步描述*/
       i++;
  }
  }
  catch(Exception e)
  { System.out.println(e);
  }
  }
  ……
  }
   多进程(线程)并发服务的一个关键问题是,如何实现进程(线程)间通信。每个客户的发言(包括表情和动作等选项)都需要放在一个公共的地方,让所有的输 出线程都能够获得它。解决的方法有很多,比如说放在数据库里,放在大家都有权限的dat文件里,或直接用管道实现进程间通信。其中,对一个聊天室服务器来 说,第一种方法是最傻的,太消耗系统资源,而且使程序执行效率变慢,可能出错环节增多。而使用管道通信的方式,把所有发言数据都保存在内存里,不但可以获 得最高的执行效率,安全的执行过程,也不用考虑线程同步的问题。不要以为所有的发言数据会很多,其实服务器端只要保存最后100句就已经很了不起了,不是 吗?
  JAVA里关于管道的API有:
  ●Java.io.PipedInputStream
  PipldInputStream():
  创建新的管道输入流,且它没有关联一个管道输出流。
  PipldInputStream(PipldOutputStream out):
  创建新的管道输入流,且从管道输出流out中读取数据。
  connect(PipldOutputStream out):
  关联一个管道输出流,且这个流读取数据。
  ●Java.io.PipedOutputStream
  PipldOutputStream():
  创建新的管道输出流,且它没有关联一个管道输入流。
  PipldOutputStream(PipldInputStream in):
  创建新的管道输出流,并输出数据到in。
  connect(PipldInputStream in):
  关联一个管道输入流,并输入数据到in。
  ◆Daemon的实现
  实际上,我还没有找到直接在JAVA中实现后台守护进程的方法。实现一个后台进程需要完成一系列的工作,包括:关闭所有的文件描述字;改变当前工作目录;重设文件存取屏蔽码(umask) ;在后台执行;脱离进程组;忽略终端I/O信号;脱离控制终端。
   JAVA中有一个叫Daemon Thread的东西,我没有使用过。据介绍,这种叫服务线程的东东唯一的目的就是为其它线程提供服务。而一个程序里如果只剩下服务线程的话,这个程序就会 停止(和我们的初衷简直就是南辕北辙)。有兴趣的朋友可以看看相关的内容,在java.lang.Thread.setDaemon()。
  虽然我们不能用JAVA实现后台服务守护进程,不过我们还有JAVA的C接口,问题总有解决的办法。
  ◆异常处理
   在Socket通信过程中很容易出现一些意外情况,如果不加处理直接发送数据,就可能导致程序意外退出。例如,客户关闭了socket后,服务器继续发 送数据,这就会导致异常。为避免这一情况的发生,我们必须对它进行处理,一般情况下,只需要简单地忽略这个信号就可以了。幸好,JAVA的异常处理机制还 比较强壮。
  ◆用户断线判断和处理
  许多情况下,用户不是通过提交“离开”按钮离开聊天室,这时候就需要判断用户是 否断线了。一般用户断线可能包括以下几种情况:方法是:当用户关闭浏览器,或者点击了浏览器stop按钮,或者跳转到其他网页的时候(如果用 JavaScript弹出一个聊天窗口的话,那么这两种情况我们是能够避免的——大不了再禁止右键),相对应的socket将会变成可读状态,而此时读出 的数据却是空字符串。
  利用这个原理,只要在某个可读的socket读取数据时,读到的却是空数据,那么我们就可以断定,与这个socket相对应的用户断线了。
  ◆防止连接超时断线
  如果浏览器在一段时间内没有接到任何数据,那么就会出现超时错误。要避免这一错误,必须在一定间隔内发送一些数据,在我们这个应用系统里,可以发送一些html注释。发送注释的工作可以直接插入聊天内容之间来完成。

[Lucene]Lucene.Net的语言处理包中Lucene.Net.Analysis.Cn的Bu

mikel阅读(610)

Lucene.Net的语言处理包中Lucene.Net.Analysis.Cn的Bug

    最近在对博客园的搜索程序进行改进。博客园的搜索功能用的是Lucene.net搜索引擎,当时博客园增加搜索功能时,Lucene.net还不支持中文分词, 后来得到http://www.cnblogs.com/yuhen/的帮助才解决这个问题。(最近博客园的搜索程序出了问题, 暂时改用google)。

    现在,Lucene.net中支持分词功能, 我下载了语言处理包(Lucene.Net NLS Pack),用其中的Lucene.Net.Analysis.Cn.ChineseAnalyzer建立索引,可是每次建立索引就出现死锁现象。看了sf.net上的这篇文章CJK Analysis maybe cause dead lock, 我想到可能是Lucene.Net.Analysis.Cn中代码有Bug,查看了Lucene.Net.Analysis.Cn中的代码,果然有问题, 在ChineseTokenizer的第148行,将dataLen == -1改为dataLen == 0, 死锁的问题就解决了。
    后来, 发现这篇文章http://ms.mblogger.cn/yysun/posts/6092.aspx中也讲到了Lucene.Net.Analysis.Cn中的Bug。
    我将两个bug改了一下,放在博客园上给需要者下载。虽然只改了两行代码,但我想改好了放在这,对一些初次使用者还是有点帮助的。
    Bin: http://files.cnblogs.com/dudu/Lucene.Net.Analysis.Cn.rar
    Src: http://files.cnblogs.com/dudu/Lucene.Net.NLS.rar

[Lucene]Lucene.net试用

mikel阅读(770)

【简介】
  lucene.net好多人都知道的吧,反正我是最近才好好的看了一下,别笑我拿历史当新闻哦,不太了解Lucence的朋友 先听我说两句哦。Lucene的知识主要分为索引、搜索、分析器、性能优化几个部分。索引和搜索没啥可说的,看几个例子就会了,来回那一套儿,按部就班做 几个实验就熟悉了。分析器是Lucence的精华,又分为分词和过滤两部分,而且中文分词更是难点,我的例子里是用从博客园程序中提取出来的 Lucene.Net.Analysis.Cn.dll来实现中文分词的,谁有中科院的那套中科院ICTCLAS分词工具的C#版麻烦提供一下哦。性能优 化也很重要,因为如果要索引的文件比较大的话,建立索引的性能就会很大的下降,你可以调整IndexWriter的几个参数来优化索引性能,还有可以用 IndexWriter.Optimize()方法(这个方法主要是优化查询速度,反而使索引性能有所下降),另外就是可以用多线程来分别对不同的内容进 行索引并保存到RAMDirectory里,然后再把所有的内存索引合并到FSDirectory里,甚至可以让多台服务器分别处理内容的各个部分,然后 把索引结果放到一个队列里,再有一台机器去读取索引结果队列并合并索引结果。
  做这个示例主要是为了演示一下Lucene.net的功能,它可 以对你指定的目录里的.txt,.htm,.html文件进行全文索引,然后对其进行查询。由于如果要索引的目录里文件特别多特别大的话,建立索引需要花 费很长的过程,所以我在示例程序里使用了异步编程,以便在建立索引的时候不阻塞界面线程。
【内容】
1、先看一个简单例子
public void Test1()
{
 //建立一个内存目录
 Lucene.Net.Store.RAMDirectory ramDir = new Lucene.Net.Store.RAMDirectory();

 //建立一个索引书写器
 IndexWriter ramWriter = new IndexWriter(ramDir,new ChineseAnalyzer(), true);

 //要索引的词,这就相当于一个个的要索引的文件
 string[] words = {"中华人民共和国", "人民共和国", "人民","共和国"}; 

 //循环数组,创建文档,给文档添加字段,并把文档添加到索引书写器里
 Document doc = null; 
 for (int i = 0; i < words.Length; i++)
 {
  doc = new Document();
  doc.Add(Field.Text("contents", words[i]));
  ramWriter.AddDocument(doc);
 }

 //索引优化
 ramWriter.Optimize();

 //关闭索引读写器,一定要关哦,按理说应该把上面的代码用try括主,在finally里关闭索引书写器
 ramWriter.Close();

 //构建一个索引搜索器
 IndexSearcher searcher = new IndexSearcher(ramDir);

 //用QueryParser.Parse方法实例化一个查询
 Query query = QueryParser.Parse("中华人民","contents",new ChineseAnalyzer());

 //获取搜索结果
 Hits hits = searcher.Search(query);

 //判断是否有搜索到的结果,当然你也可以遍历结果集并输出
 if (hits.Length() != 0)
  MessageBox.Show("有");
 else
  MessageBox.Show("没有");
}
2、其它的具体看下载代码吧。
   下载的文件里有个doc的文件夹,里面有4个文本文件,大家可以试着给那个目录建立索引,然后搜索一下“人民”,“中华”等几个关键字,看看能出来搜索 结果吗?简单说一下示例程序,就是遍历一个目录,找出所有文本和网页的文件,建立Lucene的Document文件,并索引了文件的目录和内容,然后添 加到索引器里,最后在程序执行目录的Index子目录里建立索引,这一部分的调用使用了异步委托。搜索的时候就是在Index目录里检索符合某个关键字的 条目。
【注意】
1、建立完索引后一定要调用IndexWriter的Close方法,否则如果你要索引的目录里的文件少于minMergeDocs的话,是不能建立索引的。
2、 Field.Text的静态方法有两个重载版本,如果第二个参数是string的话那么这个字段既索引也存储,如果是TextReader的话只索引不存 储,这点要搞清楚,另外在构建TextReader的时候要注意使用合适的编码格式,否则有的文件读出来是乱码,建立的索引肯定也是按乱码建立的咯。
【小节】
  其实lucene大家谁也是学学就会,关键要是整一个像google,baidu这样的搜索引擎就难了,好歹这搜索引擎也是一个行业呢,所以谁有兴趣,好好钻研一下搜索行业的相关技术,没准靠这个还能创业呢,是吧。
  再问一下,《lucence实战》有中文版吗?或者其它关于Lucence的中午图书,给推荐一本。
  最后借贵地和大家讨论一个问题:从长远考虑,程序员学那项技术比较有前途?做程序也好几年了,想找一个领域好好深入一下,以后做一个行业的领域专家。那样才不会太累,要不什么都鼓捣,太累了,而且还不容易出成绩。我列举了几个方向,大家帮忙分析分析,谢谢。
1、linux+oracle(走数据库管理的路线)
2、汇编、c底层驱动开发(据说很简单,就那么几个指令,学一年就精通了,不像.NET,得老跟着走)
3、ec++,kjava嵌入式开发(包括手机游戏,路由固件等开发)
4、即时通讯行业(网络编程,包括网络游戏的服务端编程这些)
5、搜索行业(不太了解)
6、OA、工作流(自己做一套不用编程,拖拖拽拽画画就能实现企业业务流程的电子化,infopath,OSS,formserver,WF的那一套)
7、.net网站开发(范围很大,要掌握的东西太多,会的人很多,深入的很少)
8、流媒体开发(3G时代这玩意儿不知道能不能派上用场)
【参考】
idior的《Lucene.net系列》
李刚、宋伟、邱哲的《ajax+lucene构建搜索引擎》

下载地址:
http://files.cnblogs.com/onlytiancai/WawaSearch.rar

[Flex]在线Photoshop

mikel阅读(727)

又一款在线Flex应用——在线“PhotoShop”的SumoPaint.com。这种应用目前也有一些例子了,例如很早前介绍的Pixlr.com。本文介绍的这款应用也很不错,基本的功能都有了,当然,要跟PhotoShop比还差得远。现在这种应用感觉不是很热,真的是搞设计的都宁愿用桌面版PS、FW那些。希望在以后这种应用会火起来。(PS:期待Google的gos推出) 网址:http://www.sumopaint.com/app/

[C#]应用程序架构指导袖珍版

mikel阅读(745)

微软模式与实践小组最近发布了应用程序架构指导袖珍版本,总共有6本,分别介绍了不同类型应用程序的架构指导,包括敏捷架构方法、Mobile应用程序、RIA应用程序、富客户端应用程序、Web应用程序以及Service架构等,非常值得一看。

1. 敏捷架构方法

App_Guide_Agile

下载指导

2. Web引用架构

App_Guide_Web

下载指导

3. 移动应用架构

App_Guide_Mobile

下载指导

4. RIA架构

App_Guide_RIA

下载指导

5. 富客户端架构

App_Guide_RCA

下载指导

6. 服务架构

App_Guide_Service

下载指导

作者:TerryLee
出处:http://terrylee.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

[网站]最古怪的网站列表

mikel阅读(678)

10大最佳古怪网站
  【网站名称】:眼睛的幻觉
  【网站链接】:http://www.michaelbach.de/ot/index.html
  【网站简介】:在这里你可以体验各种“空间频率扭曲”,实际上那只是“你的眼睛背叛了你的心”而已
  【网站名称】:路边收集衣
  【网站链接】:http://www.pavementgear.com/
  【网站简介】:该网站专门收集路边被人丢弃的衣物,但他绝对不是捡LJ的,我们怀疑该网站的创始人是个有怪癖的家伙。
  【网站名称】:粗口合集
  【网站链接】:http://www.rosskemp.co.uk/
  【网站简介】:为了表达对粗口和暴力精神的崇敬,以及对著名艺人RossKemp的欣赏和遗憾,他的Fans整了这么一个只有他的头像和他“名言”的网站。
  【网站名称】:打进富人榜
  【网站链接】:http://www.globalrichlist.com/
  【网站简介】:输入收入水平,看看你在地球的财富排行中数老几。很有可能你会惊奇地发现,自己居然属于高收入人群。
  【网站名称】:厕所博物馆
  【网站链接】:http://www.toiletmuseum.com/
  【网站简介】:从厕所涂鸦到马桶设计,从两性如厕研究到厕所的社会功能,我们大便的地方其实并不那么单纯……当然,一定不能错过裘·德洛等一干名人蹲马桶时的情景
  【网站名称】:伤疤比赛
  【网站链接】:http://www.scarmageddon.com/
  【网站简介】:男人就喜欢显摆自己的伤疤,因此应该找个地方让他们比比。在这里,你可以找到来自世界各地的对手。不过老实说,你“六岁时从炕上掉下来”留下的伤疤和这里的前十名相比可能只是一个小针眼而已。
  【网站名称】:妈妈说就算你注册的域名再长baidu都能搜索出来,对应网址看看
  【网站链接】:http://www.mamashuojiusuannizhuc…engsousuochulai.cn/
  【网站简介】:据说这个域名是百度的员工注册的,点开一看,果然是百度。
  【网站名称】:博客分析机
  【网站链接】:http://www.bigfools.com/quiz/blog.php
  【网站简介】:编辑小马用自己的博客试了试,分析结果说他的博客有38%以上自恋成分,没错,小马承认了,你也来试试吧!
  
  【网站名称】:国际爱好洗碗池边进餐者协会
  【网站链接】:http://www.sinkie.com/
  【网站简介】:据说在洗碗池旁吃饭是一种新的健康饮食方式,鬼晓得是不是真的。不过,可以肯定的是这个网站不仅有众多会员,而且还为此设立了自己的节日。
  【网站名称】:可笑可乐
  【网站链接】:http://www.shakeskin.com/
  【网站简介】:用吃奶的力气做个鬼脸,然后拍成照片传到网上。结果你得到一个印着你的头像的杯子,而你的脸比杯子把儿弯曲得还厉害。
  10大最佳实用网站
  【网站名称】:经典桌面
  【网站链接】:http://www.mydeskcity.com/
  【网站简介】:这个名叫东子的偏执狂不知花了多少工夫搜集这些图片,有将近40G的容量,而且很多图片都是站长本人在国外搜集后,自己进行加工的作品,值得一看。
  【网站名称】:查询在线
  【网站链接】:http://www.answers.com/
  【网站简介】:别老想着谷歌和百度,也可以试试这个在线词典,小到词语大到事件,都能找到详细的解释和相关的网站链接,而且有不同的语言版本。
  【网站名称】:MD主机
  【网站链接】:http://www.host.md/
  【网站简介】:MD??哪个国家的域名?其实是一个空间提供商,有免费空间,还可以注册xxx.host.md二级域名【网站名称】:网络精选
  【网站链接】:http://www.kontraband.com/
  【网站简介】:住在伦敦的网络老油条理查德创建了这个网站,上面包含网络上最好的视频剪辑、游戏、姑娘和图片。这里泡上整整一个下午,也不一定能真正看完这儿全部宝贝的九牛一毛。
  【网站名称】:想说就说
  【网站链接】:http://www.skype.com/
  【网站简介】:这个软件结合了视频电话和MSN的所有功能,还可以拨打国际长途。在国外火得一塌糊涂,国内知道的人就不多了,据说是因为费用太便宜,引来某些本土通讯大哥们的联合抵制。
  【网站名称】:抓瞎
  【网站链接】:http://www.zhuaxia.com/
  【网站简介】:不是抓虾!一个为你精选挑选各种网络资讯的网站,确实省不少时间。
  【网站名称】:个人空间
  【网站链接】:http://www.myspace.com/
  【网站简介】:有个美国佬最近把50多亿美金都扔在一个网站上,由此我们可以预见到互联网的未来。赶紧找个空间创建你自己的天地吧,把你的照片、音乐传上去,再搞点小买卖。
  【网站名称】:让聊天随时随地
  【网站链接】:http://www.gabbly.com/
  【网站简介】:在任何网站上都可以聊天,只要在前面加“gabbly.com”。【网站名称】:中奖发财
  【网站链接】:http://www.zhuaj.com/
  【网站简介】:钱不够花,只能说明你野路子不够多。不妨来这个抓奖网站碰碰运气,几乎所有中奖的活动都收纳其中,接下来就看你有没有那个运气了.
  【网站名称】:图片共享现在连
  【网站链接】:http://www.flickr.com/
  【网站简介】:照片的上传和分享都有软件可以帮你打理完成了.下载这个免费的软件,然后往里面填好你的链接地址,就可以和全世界的朋友们分享了。如果你乐意,你甚至可以把它们变成海报、画册、甚至是邮票,当然也可以自由浏览他人的作品。
  10个最佳创意网站
  【网站名称】:永远在变的video
  【网站链接】:http://www.theunseenvideo.com/
  【网站简介】:它会读取你的IP地址,以及当地的天气和温度,不同的气候条件下你看到的MV是不一样的……
  【网站名称】:文字游戏
  【网站链接】:http://www.bemboszoo.com/
  【网站简介】:只用二十六个英文字母就创造出了一个趣味盎然的动物世界,所有动物都极其巧妙的由其英文名字的字母构成,而且所有的字母只采用了BemboRoman这一种字体,变化无穷,令人眼花缭乱。
  【网站名称】:要你命2007
  【网站链接】:http://www.meebo.com/
  【网站简介】:一个小暴发户网站,它把Yahoo、Jabber、Gtalk、AIM、ICQ和MSN几款流行的即时通讯软件整合在一起,支持多款软件之间的信息互通,是名副其实的“聊天要你命2007”。
  【网站名称】:网不易
  【网站链接】:http://www.netnoease.com/
  【网站简介】:这可不是网易的姊妹站也不是死对头,据说是由两个刚毕业的大学生创作的个人网站。这个网站的特点既不是新闻即时也不是游戏火爆,而是回复量惊人,口水也不少。
  【网站名称】:外国家谱
  【网站链接】:http://www.geni.com/tree/start
  【网站简介】:不要以为只有中国人有家族谱的概念,国外也有这一套。不过国内是以家族为基础,国外就不管这一套了。在这个家族谱网站理,不分国界,全球参与。
  【网站名称】:年龄猜想
  【网站链接】:http://www.ageguess.com/
  【网站简介】:如果你很在意真实的你在别人眼中的形象,那就提交相片到这里吧,浏览网页的陌生人会告诉你真正看起来有多大……陌生的评价最客观。
  【网站名称】:百GOOGLE度
  【网站链接】:http://www.baigoogledu.com/
  【网站简介】:百度和谷歌野合的产物,据说他们之间平均85%的链接都不一样。
  【网站名称】:卖衣服
  【网站链接】:http://www.uniqlo.com/
  【网站简介】:你一定想不到这家网站是做什么的,有点儿像搞视觉艺术,但你会发现原来你可以在这买到衣服。没错,它就是卖衣服的。
  【网站名称】:工业设计前线
  【网站链接】:http://www.yankodesign.com/
  【网站简介】:不蒙你,这里能找到全球最新的工业产品设计,看了你就知道原来自己的生活质量有多差。
  【网站名称】:百万美元
  【网站链接】:http://www.milliondollarhomepage.com/
  【网站简介】:电脑画面上大约有100万个像素,如果每个象素以1美元卖给不同的客户,就可以赚到100万美元。梦话?一位叫AlexTew的英国男孩做到了。
  10个最佳视听网站
  【网站名称】:有些电影不过是烂片
  【网站链接】:http://www.rottentomatoes.com/
  【网站简介】:这里评选出的是最烂的片子,并用“一般烂、很烂、超级烂”这样的级别给它们分类。参评作品中甚至还包括了一些电视游戏,显然评委们有些激动了。
  【网站名称】:金色视频
  【网站链接】:http://www.wfuv.org/
  【网站简介】:就如同在你的电脑上开通了六万个电视频道一样,你只要点点鼠标就可以观看到各种电视节目,从体育到戏剧,共有100万小时的剪辑供你免费观看。
  【网站名称】:逆音
  【网站链接】:http://www.indieray.com
  【网站简介】:闲人一律免进,只献给那些热爱摇滚的孩子们。说不定……你就是其中之一
  【网站名称】:好莱坞电影手册
  【网站链接】:http//myvideo.blogbus.com
  【网站简介】:不管好莱坞的电影再怎么傻X,你就是愿意让他们从你口袋里掏钱。既然如此,我们决定帮你把二百五的热情进行到底。没错,绝招就在这个好莱坞电影手册。
  【网站名称】:中式英语
  【网站链接】:http://www.chinglish.de/
  【网站简介】:干货翻译成“FUCKGOODS”,难怪老外一头雾水,而这样的例子可远远不在少数。拜托,明年就要奥运了,别这么丢脸了行不行啊!
  【网站名称】:跳舞
  【网站链接】:http://b.muglets.com/
  【网站简介】:我们猜测,叫每个打开此网页的人都哈哈大笑是站长的目的。上传或者选择一个头像,然后选择一个身体,再然后就让这个小兄弟在你的屏幕上不停地跳舞吧!
  【网站名称】:冷笑话
  【网站链接】:http://www.douban.com/group/Giaclub
  【网站简介】:笑话居然也有冷热之分,而且冷笑话照样让你笑个不停。当然你可以不信这个邪,因为在他们看来你压根就是一个不懂幽默的人。
  【网站名称】:每日笑报
  【网站链接】:http://www.dailyfunnypics.com
  【网站简介】:全世界各地的搞笑达人集体奉献,各种搞怪的漫画、涂鸦、视频应有尽有,日日更新。
  【网站名称】:洋葱
  【网站链接】:http://www.theonion.com/content/index
  【网站简介】:如果有人说这是美国最好的新闻网站,你可千万别信。因为……网站上所有的新闻都是假的,没错,他们是故意的。还不懂吗,生活的最大的笑话就是你认为可信的新闻居然都是假的。
  【网站名称】:搞笑游戏
  【网站链接】:http://www.funny-games.ws
  【网站简介】:上千个搞笑游戏,每日更新,你要做的就是玩了之后哈哈大笑。就是不好笑,也假装意思一下嘛。
  【网站名称】:最搞笑的时光
  【网站链接】:http://www.killsometime.com/
  【网站简介】:各种无厘头的搞笑视频,以及搞笑游戏,不能算是最好的,但很有意思,有空过来看看,当然,前提是你实在闲得没事。
  【网站名称】:我很烦
  【网站链接】:http://www.i-am-bored.com/
  【网站简介】:你很烦,别理你?那哪成,万一你想不开跳楼呢!看看这个网站吧,世界上可不是只有你一个人烦,看看别人是怎么消解无聊的。
  【网站名称】:快乐至死
  【网站链接】:http://www.htportal.org/websites.html
  【网站简介】:搞笑视频、离奇经历、傻x电影,难道让你笑一下真的很难吗?我们不信这个邪!
  【网站名称】:很无聊
  【网站链接】:http://www.whangdoodle.us/links.htm
  【网站简介】:很多滑稽的图片和视频,并且链接了为数不少的其他搞笑网站。
  10大最佳恋物网站
  【网站名称】:罗博报告
  【网站链接】:http://www.robbreport.com/
  【网站简介】:顶级奢侈品的圣经,世界富豪们的每月必读之物。你想拥有吗?恭喜你,今年10月份这本杂志就要登陆中国了,前提是你拥有至少5000万的身价,否则你根本就买不到,因为不会在市场上发行。
  【网站名称】:在线购物
  【网站链接】:http://www.galleria.com.cn/
  【网站简介】:舍得花钱,懒得出门?这里是个好去处。只要保证你的卡里有足够的闲钱,第二天就会有人送货上门。
  【网站名称】:我爱奢侈品
  【网站链接】:http://www.ishechi.com/
  【网站简介】:一个不错的奢侈品综合网站,一干为数众多的奢侈品拥孬,成为他们的会员吧,比你一个人待家里看毛片来得有趣!
  【网站名称】:男人玩具
  【网站链接】:http://www.topgear.com/
  【网站简介】:是谁说汽车就是男人的生殖器来着,这人一定是个天才。准备点冰块把,以防肾上腺控制不住节奏。
  【网站名称】:奢华生活
  【网站链接】:http://www.luxurylifestyle.com/
  【网站简介】:等咱有钱了,直升飞机买两架,一架挂着另一架……打住吧,兄弟,这是暴发户,不是真正的奢华生活。
  【网站名称】:时间区
  【网站链接】:http://www.timezone.com/
  【网站简介】:老实说,造一只顶级的世界好表,可一点儿都不比生个孩子容易,甚至时间更长。所以,抽点时间好好补补课吧
  【网站名称】:万宝龙
  【网站链接】:http://www.montblanc.com/
  【网站简介】:不管你信不信,现在连笔都成奢侈品了,据说最贵的笔都能上千万。当然,品质同样重要,别中看不中
  【网站名称】:抽根雪茄
  【网站链接】:http://www.cubancrafters.com/top10.php
  【网站简介】:你别不服气,世界上最好的雪茄只能产自古巴。这个网站每年会评出年度10种顶级的古巴雪茄,光是看着就叫人心里痒痒。
  【网站名称】:电子集中营
  【网站链接】:http://www.engadget.com/
  【网站简介】:一小撮电子产品狂热分子建起了这个网站准备自娱自乐,没想到居然人气越来越旺。老实说,里面的东西还确实不错。
  【网站名称】:杯酒人生
  【网站链接】:http://www.winefond.com/
  【网站简介】:一个专业的葡萄酒试酒网站,不管是哪个年份,哪个国家,哪种酒,你都能得到详细的资料。当然,我们想让你干得可远远不止这个……