精华内容
下载资源
问答
  • Socket通信

    万次阅读 2016-03-24 20:26:11
    Socket理论知识OSI七层网络模型OSI七层网络模型(从下往上): OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊 的网络功能,从网络功能角度观察: 下面4层(物理层、...

    Socket理论知识

    OSI七层网络模型

    OSI七层网络模型(从下往上):
    这里写图片描述
    这里写图片描述

    OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊 的网络功能,从网络功能角度观察:

    • 下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能, 即以节点到节点之间的通信为主
      第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;
    • 上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主

    简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。
    OSI七层模型详解

    TCP/IP四层模型

    这里写图片描述

    TCP/UDP区别

    这里写图片描述

    这里写图片描述

    Socket开发相关的一些概念名词:

    IP地址

    IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
    

    端口

    • 用于区分不同的应用程序
    • 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!
    • IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点, 是TCP和UDP的基础!
    • 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23

    TCP协议与UDP协议

    TCP协议流程详解

    首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议, 因为TCP,IP协议是两个很重要的协议,就用他两命名了。
    下面我们来讲解TCP协议和UDP协议的区别:
    TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱 ,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手

    三次握手: 建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立, 在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:

    这里写图片描述

    • 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server, Client进入SYN_SENT状态,等待Server确认。
    • 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位 SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求 ,Server进入SYN_RCVD状态。
    • 第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK 置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则 连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以 开始传输数据了。

    四次挥手: 终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。 在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下

    这里写图片描述

    • 第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入 FIN_WAIT_1状态
    • 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同, 一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
    • 第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK 状态。
    • 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。 另外也可能是同时发起主动关闭的情况:

    这里写图片描述

    另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢? 答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里 发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还 能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些 数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会 分开发送。

    UDP协议详解

    UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不 建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽 的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。 相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包

    Java中对于网络提供的几个关键类:

    针对不同的网络通信层次,Java给我们提供的网络功能有四大类:

    • InetAddress: 用于标识网络上的硬件资源
    • URL: 统一资源定位符,通过URL可以直接读取或者写入网络上的数据
    • Socket和ServerSocket: 使用TCP协议实现网络通信的Socket相关的类
    • Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信

    InetAddress:

    public class InetAddressTest {
        public static void main(String[] args) throws Exception{
            //获取本机InetAddress的实例:
            InetAddress address = InetAddress.getLocalHost();
            System.out.println("本机名:" + address.getHostName());
            System.out.println("IP地址:" + address.getHostAddress());
            byte[] bytes = address.getAddress();
            System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
            System.out.println("直接输出InetAddress对象:" + address);
        }
    }

    基于TCP协议的Socket通信-简易聊天室

    基本介绍和使用

    什么是Socket

    这里写图片描述

    Socket通信模型

    这里写图片描述

    Socket通信实现步骤解析:

    • Step 1:创建ServerSocket和Socket
    • Step 2:打开连接到的Socket的输入/输出流
    • Step 3:按照协议对Socket进行读/写操作
    • Step 4:关闭输入输出流,以及Socket

    我们接下来写一个简单的例子,开启服务端后,客户端点击按钮然后链接服务端, 并向服务端发送一串字符串,表示通过Socket链接上服务器~

    Socket服务端的编写

    步骤

    Step 1:创建ServerSocket对象,绑定监听的端口
    Step 2:调用accept()方法监听客户端的请求
    Step 3:连接建立后,通过输入流读取客户端发送的请求信息
    Step 4:通过输出流向客户端发送响应信息
    Step 5:关闭相关资源

    Code

    在Eclipse下创建一个Java项目,代码如下:

    package com.turing.server;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * Step 1:创建ServerSocket对象,绑定监听的端口 
     * Step 2:调用accept()方法监听客户端的请求
     * Step 3:连接建立后,通过输入流读取客户端发送的请求信息 
     * Step 4:通过输出流向客户端发送响应信息 
     * Step 5:关闭相关资源
     *  *
     */
    public class SocketServer {
    
        public static void main(String[] args) {
    
            try {
                // 1. 创建一个服务端Socket,即ServerSocket,指定绑定的端口,并监听此端口
                ServerSocket serverSocket = new ServerSocket(9999);
                // 获取服务端IP
                InetAddress inetAddress = InetAddress.getLocalHost();
                String ip = inetAddress.getHostAddress();
                System.out.println("~~~服务端已就绪,等待客户端接入~,服务端ip地址: " + ip);
                // 2. 调用accept等待客户端连接
                Socket socket = serverSocket.accept();
                // 3. 连接后获取输入流,读取客户端信息
                InputStream is = null;
                InputStreamReader isr = null;
                BufferedReader br = null;
                OutputStream os = null;
                PrintWriter pw = null;
                // 获取输入流
                is = socket.getInputStream();
                isr = new InputStreamReader(is, "UTF-8");
                br = new BufferedReader(isr);
    
                String info = null;
                while ((info = br.readLine()) != null) {
                    System.out.println("客户端发送过来的信息" + info);
                }
    
                socket.shutdownInput();// 关闭输入流
                socket.close();
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    

    运行服务端
    这里写图片描述

    Socket客户端的编写

    Android客户端

    步骤

    Step 1:创建Socket对象,指明需要链接的服务器的地址和端号
    Step 2:链接建立后,通过输出流向服务器发送请求信息
    Step 3:通过输出流获取服务器响应的信息
    Step 4:关闭相关资源

    Code

    package com.turing.base.activity.socket.baseuse;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.Button;
    
    import com.turing.base.R;
    import com.turing.base.utils.AlertUtil;
    
    import java.io.IOException;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.net.InetAddress;
    import java.net.Socket;
    
    /**
     * Step 1:创建Socket对象,指明需要链接的服务器的地址和端号
     Step 2:链接建立后,通过输出流向服务器发送请求信息
     Step 3:通过输出流获取服务器响应的信息
     Step 4:关闭相关资源
    
     服务端Java工程:D:\workspace\ws-java-base\SocketServer
     */
    public class SocketClientAct extends AppCompatActivity implements View.OnClickListener {
    
        private Button socketSend ;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_socket_client);
    
            socketSend = (Button) findViewById(R.id.id_btn_sendMsg);
            socketSend.setOnClickListener(this);
    
        }
    
        @Override
        public void onClick(View v) {
    
            AlertUtil.showToastShort(this,"观察服务端日志~");
            //Android不允许在主线程(UI线程)中做网络操作,
            // 所以这里需要我们自己 另开一个线程来连接Socket!
            new Thread() {
                @Override
                public void run() {
                    try {
                        acceptServer();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    
        /**
         * 连接ServerSocket发送消息
         */
        private void acceptServer() throws  IOException{
    
            //1.创建客户端Socket,指定服务器地址和端口
            Socket socket = new Socket("192.168.56.1", 9999);
            //2.获取输出流,向服务器端发送信息
            OutputStream os = socket.getOutputStream();//字节输出流
            PrintWriter pw = new PrintWriter(os);//将输出流包装为打印流
            //获取客户端的IP地址
            InetAddress address = InetAddress.getLocalHost();
            String ip = address.getHostAddress();
            // 将这个信息发送给服务端
            pw.write("客户端:~" + ip + "~ 接入服务器!!");
            pw.flush();
            socket.shutdownOutput();//关闭输出流
            socket.close();
        }
    }
    

    因为Android不允许在主线程(UI线程)中做网络操作,所以这里需要我们自己 另开一个线程来连接Socket!

    运行结果:
    点击按钮后,服务端控制台打印

    这里写图片描述

    简易聊天室

    简易聊天室

    基于Socket完成大文件的断点续传

    断点续传


    基于UDP协议的Socket通信

    UDP以数据报作为数据的传输载体,在进行传输时 首先要把传输的数据定义成数据报(Datagram),在数据报中指明数据要到达的Socket(主机地址 和端口号),然后再将数据以数据报的形式发送出去,然后就没有然后了,服务端收不收到我就 不知道了,除非服务端收到后又给我回一段确认的数据报。

    服务端实现步骤

    Step 1:创建DatagramSocket,指定端口号
    Step 2:创建DatagramPacket
    Step 3:接收客户端发送的数据信息
    Step 4:读取数据

    public class UPDServer {
        public static void main(String[] args) throws IOException {
            /*
             * 接收客户端发送的数据
             */
            // 1.创建服务器端DatagramSocket,指定端口
            DatagramSocket socket = new DatagramSocket(12345);
            // 2.创建数据报,用于接收客户端发送的数据
            byte[] data = new byte[1024];// 创建字节数组,指定接收的数据包的大小
            DatagramPacket packet = new DatagramPacket(data, data.length);
            // 3.接收客户端发送的数据
            System.out.println("****服务器端已经启动,等待客户端发送数据");
            socket.receive(packet);// 此方法在接收到数据报之前会一直阻塞
            // 4.读取数据
            String info = new String(data, 0, packet.getLength());
            System.out.println("我是服务器,客户端说:" + info);
    
            /*
             * 向客户端响应数据
             */
            // 1.定义客户端的地址、端口号、数据
            InetAddress address = packet.getAddress();
            int port = packet.getPort();
            byte[] data2 = "欢迎您!".getBytes();
            // 2.创建数据报,包含响应的数据信息
            DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
            // 3.响应客户端
            socket.send(packet2);
            // 4.关闭资源
            socket.close();
        }
    }

    客户端实现步骤

    Step 1:定义发送信息
    Step 2:创建DatagramPacket,包含将要发送的信息
    Step 3:创建DatagramSocket
    Step 4:发送数据

    public class UDPClient {
        public static void main(String[] args) throws IOException {
            /*
             * 向服务器端发送数据
             */
            // 1.定义服务器的地址、端口号、数据
            InetAddress address = InetAddress.getByName("localhost");
            int port = 8800;
            byte[] data = "用户名:admin;密码:123".getBytes();
            // 2.创建数据报,包含发送的数据信息
            DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
            // 3.创建DatagramSocket对象
            DatagramSocket socket = new DatagramSocket();
            // 4.向服务器端发送数据报
            socket.send(packet);
    
            /*
             * 接收服务器端响应的数据
             */
            // 1.创建数据报,用于接收服务器端响应的数据
            byte[] data2 = new byte[1024];
            DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
            // 2.接收服务器响应的数据
            socket.receive(packet2);
            // 3.读取数据
            String reply = new String(data2, 0, packet2.getLength());
            System.out.println("我是客户端,服务器说:" + reply);
            // 4.关闭资源
            socket.close();
        }
    }

    总结

    将数据转换为字节,然后放到DatagramPacket(数据报包中),发送的 时候带上接受者的IP地址和端口号,而接收时,用一个字节数组来缓存!发送的时候需要创建一个 DatagramSocket(端到端通信的类)对象,然后调用send方法给接受者发送数据报包

    展开全文
  • socket通信

    千次阅读 2016-03-31 17:55:25
    通信域用来说明套接口通信的协议,不同的通信域有不同的通信协议以及套接口的地址结构等等,因此,创建一个套接口时,要指明它的通信域。比较常见的是unix域套接口(采用套接口机制实现单机内的进程间通信)及网际...

    一个套接口可以看作是进程间通信的端点(endpoint),每个套接口的名字都是唯一的(唯一的含义是不言而喻的),其他进程可以发现、连接并且与之通信。通信域用来说明套接口通信的协议,不同的通信域有不同的通信协议以及套接口的地址结构等等,因此,创建一个套接口时,要指明它的通信域。比较常见的是unix域套接口(采用套接口机制实现单机内的进程间通信)及网际通信域。

    1、背景知识

    linux目前的网络内核代码主要基于伯克利的BSD的unix实现,整个结构采用的是一种面向对象的分层机制。层与层之间有严格的接口定义。这里我们引用[1]中的一个图表来描述linux支持的一些通信协议:

    我们这里只关心IPS,即因特网协议族,也就是通常所说的TCP/IP网络。我们这里假设读者具有网络方面的一些背景知识,如了解网络的分层结构,通常所说的7层结构;了解IP地址以及路由的一些基本知识。

    目前linux网络API是基于BSD套接口的(系统V提供基于流I/O子系统的用户接口,但是linux内核目前不支持流I/O子系统)。套接口可以说是网络编程中一个非常重要的概念,linux以文件的形式实现套接口,与套接口相应的文件属于sockfs特殊文件系统,创建一个套接口就是在sockfs中创建一个特殊文件,并建立起为实现套接口功能的相关数据结构。换句话说,对每一个新创建的BSD套接口,linux内核都将在sockfs特殊文件系统中创建一个新的inode。描述套接口的数据结构是socket,将在后面给出。

    2、重要数据结构

    下面是在网络编程中比较重要的几个数据结构,读者可以在后面介绍编程API部分再回过头来了解它们。

    (1)表示套接口的数据结构struct socket

    套接口是由socket数据结构代表的,形式如下:

    struct socket
    {
    socket_state  state;     /* 指明套接口的连接状态,一个套接口的连接状态可以有以下几种
    套接口是空闲的,还没有进行相应的端口及地址的绑定;还没有连接;正在连接中;已经连接;正在解除连接。 */
      unsigned long    flags;
      struct proto_ops  ops;  /* 指明可对套接口进行的各种操作 */
      struct inode    inode;    /* 指向sockfs文件系统中的相应inode */
      struct fasync_struct  *fasync_list;  /* Asynchronous wake up list  */
      struct file    *file;          /* 指向sockfs文件系统中的相应文件  */
    struct sock    sk;  /* 任何协议族都有其特定的套接口特性,该域就指向特定协议族的套接口对
    象。 */
      wait_queue_head_t  wait;
      short      type;
      unsigned char    passcred;
    };

    (2)描述套接口通用地址的数据结构struct sockaddr

    由于历史的缘故,在bind、connect等系统调用中,特定于协议的套接口地址结构指针都要强制转换成该通用的套接口地址结构指针。结构形式如下:

    struct sockaddr {
    	sa_family_t	sa_family;	/* address family, AF_xxx	*/
    	char		sa_data[14];	/* 14 bytes of protocol address	*/
    };

    (3)描述因特网地址结构的数据结构struct sockaddr_in(这里局限于IP4):

    struct sockaddr_in
      {
        __SOCKADDR_COMMON (sin_);	/* 描述协议族 */
        in_port_t sin_port;			/* 端口号 */
        struct in_addr sin_addr;		/* 因特网地址 */
        /* Pad to size of `struct sockaddr'.  */
        unsigned char sin_zero[sizeof (struct sockaddr) -
    			   __SOCKADDR_COMMON_SIZE -
    			   sizeof (in_port_t) -
    			   sizeof (struct in_addr)];
      };

    一般来说,读者最关心的是前三个域,即通信协议、端口号及地址。

    3、套接口编程的几个重要步骤:

    (1)创建套接口,由系统调用socket实现:

    int socket( int domain, int type, int ptotocol);

    参数domain指明通信域,如PF_UNIX(unix域),PF_INET(IPv4),PF_INET6(IPv6)等;type指明通信类型,如SOCK_STREAM(面向连接方式)、SOCK_DGRAM(非面向连接方式)等。一般来说,参数protocol可设置为0,除非用在原始套接口上(原始套接口有一些特殊功能,后面还将介绍)。

    注:socket()系统调用为套接口在sockfs文件系统中分配一个新的文件和dentry对象,并通过文件描述符把它们与调用进程联系起来。进程可以像访问一个已经打开的文件一样访问套接口在sockfs中的对应文件。但进程绝不能调用open()来访问该文件(sockfs文件系统没有可视安装点,其中的文件永远不会出现在系统目录树上),当套接口被关闭时,内核会自动删除sockfs中的inodes。

    (2)绑定地址

    根据传输层协议(TCP、UDP)的不同,客户机及服务器的处理方式也有很大不同。但是,不管通信双方使用何种传输协议,都需要一种标识自己的机制。

    通信双方一般由两个方面标识:地址和端口号(通常,一个IP地址和一个端口号常常被称为一个套接口)。根据地址可以寻址到主机,根据端口号则可以寻址到主机提供特定服务的进程,实际上,一个特定的端口号代表了一个提供特定服务的进程。

    对于使用TCP传输协议通信方式来说,通信双方需要给自己绑定一个唯一标识自己的套接口,以便建立连接;对于使用UDP传输协议,只需要服务器绑定一个标识自己的套接口就可以了,用户则不需要绑定(在需要时,如调用connect时[注1],内核会自动分配一个本地地址和本地端口号)。绑定操作由系统调用bind()完成:

    int bind( int sockfd, const struct sockaddr * my_addr, socklen_t my_addr_len)

    第二个参数对于Ipv4来说,实际上需要填充的结构是struct sockaddr_in,前面已经介绍了该结构。这里只想强调该结构的第一个域,它表明该套接口使用的通信协议,如AF_INET。联系socket系统调用的第一个参数,读者可能会想到PF_INET与AF_INET究竟有什么不同?实际上,原来的想法是每个通信域(如PF_INET)可能对应多个协议(如AF_INET),而事实上支持多个协议的通信域一直没有实现。因此,在linux内核中,AF_***与PF_***被定义为同一个常数,因此,在编程时可以不加区分地使用他们。

    注1:在采用非面向连接通信方式时,也会用到connect()调用,不过与在面向连接中的connect()调用有本质的区别:在非面向连接通信中,connect调用只是先设置一下对方的地址,内核为本地套接口记下对方的地址,然后采用send()来发送数据,这样避免每次发送时都要提供相同的目的地址。其中的connect()调用不涉及握手过程;而在面向连接的通信方式中,connect()要完成一个严格的握手过程。

    (3)请求建立连接(由TCP客户发起)

    对于采用面向连接的传输协议TCP实现通信来说,一个比较重要的步骤就是通信双方建立连接(如果采用udp传输协议则不需要),由系统调用connect()完成:

    int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)

    第一个参数为本地调用socket后返回的描述符,第二个参数为服务器的地址结构指针。connect()向指定的套接口请求建立连接。

    注:与connect()相对应,在服务器端,通过系统调用listen(),指定服务器端的套接口为监听套接口,监听每一个向服务器套接口发出的连接请求,并通过握手机制建立连接。内核为listen()维护两个队列:已完成连接队列和未完成连接队列。

    (4)接受连接请求(由TCP服务器端发起)

    服务器端通过监听套接口,为所有连接请求建立了两个队列:已完成连接队列和未完成连接队列(每个监听套接口都对应这样两个队列,当然,一般服务器只有一个监听套接口)。通过accept()调用,服务器将在监听套接口的已连接队列头中,返回用于代表当前连接的套接口描述字。

    int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen)

    第一个参数指明哪个监听套接口,一般是由listen()系统调用指定的(由于每个监听套接口都对应已连接和未连接两个队列,因此它的内部机制实质是通过sockfd指定在哪个已连接队列头中返回一个用于当前客户的连接,如果相应的已连接队列为空,accept进入睡眠)。第二个参数指明客户的地址结构,如果对客户的身份不感兴趣,可指定其为空。

    注:对于采用TCP传输协议进行通信的服务器和客户机来说,一定要经过客户请求建立连接,服务器接受连接请求这一过程;而对采用UDP传输协议的通信双方则不需要这一步骤。

    (5)通信

    客户机可以通过套接口接收服务器传过来的数据,也可以通过套接口向服务器发送数据。前面所有的准备工作(创建套接口、绑定等操作)都是为这一步骤准备的。

    常用的从套接口中接收数据的调用有:recv、recvfrom、recvmsg等,常用的向套接口中发送数据的调用有send、sendto、sendmsg等。

    int recv(int s, void *
            buf, size_t 
            len, int 
            flags)
    int recvfrom(int s,  void *
            buf,  size_t 
            len, int 
            flags, struct sockaddr *
            from, socklen_t *
            fromlen)
    int recvmsg(int s, struct msghdr *
            msg, int 
            flags)
    int send(int s,const void *
            msg, size_t 
            len, int 
            flags)
    int sendto(int s, const void *
            msg, size_t 
            len, int 
            flags const struct sockaddr *
            to, socklen_t 
            tolen)
    int sendmsg(int s, const struct msghdr *
            msg, int 
            flags)

    这里不再对这些调用作具体的说明,只想强调一下,recvfrom()以及recvmsg()可用于面向连接的套接口,也可用于面向非连接的套接口;而recv()一般用于面向连接的套接口。另外,在调用了connect()之后,就应给调用send()而不是sendto()了,因为调用了connect之后,目标就已经确定了。

    前面讲到,socket()系统调用返回套接口描述字,实际上它是一个文件描述符。所以,可以对套接口进行通常的读写操作,即使用read()及write()方法。在实际应用中,由于面向连接的通信(采用TCP传输协议)是可靠的,同时又保证字节流原有的顺序,所以更适合用read及write方法。而非面向连接的通信(采用UDP传输协议)是不可靠的,字节流也不一定保持原有的顺序,所以一般不宜用read及write方法。

    (6)通信的最后一步是关闭套接口

    由close()来完成此项功能,它唯一的参数是套接口描述字,不再赘述。

    4、典型调用代码:

    到处可以发现基于套接口的客户机及服务器程序,这里不再给出完整的范例代码,只是给出它们的典型调用代码,并给出简要说明。

    (1)典型的TCP服务器代码:

    ... ...
    int listen_fd, connect_fd;
    struct sockaddr_in serv_addr, client_addr;
    ... ...
    listen_fd = socket ( PF_INET, SOCK_STREAM, 0 );
    
    /* 创建网际Ipv4域的(由PF_INET指定)面向连接的(由SOCK_STREAM指定,
    如果创建非面向连接的套接口则指定为SOCK_DGRAM)
    的套接口。第三个参数0表示由内核确定缺省的传输协议,
    对于本例,由于创建的是可靠的面向连接的基于流的套接口,
    内核将选择TCP作为本套接口的传输协议) */
    
    bzero( &serv_addr, sizeof(serv_addr) );
    serv_addr.sin_family = AF_INET ;  /* 指明通信协议族 */
    serv_addr.sin_port = htons( 49152 ) ;       /* 分配端口号 */
    inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;
    /* 分配地址,把点分十进制IPv4地址转化为32位二进制Ipv4地址。 */
    bind( listen_fd, (struct sockaddr*) serv_addr, sizeof ( struct sockaddr_in )) ; 
    /* 实现绑定操作 */
    listen( listen_fd, max_num) ; 
    /* 套接口进入侦听状态,max_num规定了内核为此套接口排队的最大连接个数 */
    for( ; ; ) {
    ... ...
    connect_fd = accept( listen_fd, (struct sockaddr*)client_addr, &len ) ; /* 获得连接fd. */
    ... ...					/* 发送和接收数据 */
    }

    注:端口号的分配是有一些惯例的,不同的端口号对应不同的服务或进程。比如一般都把端口号21分配给FTP服务器的TCP/IP实现。端口号一般分为3段,0-1023(受限的众所周知的端口,由分配数值的权威机构IANA管理),1024-49151(可以从IANA那里申请注册的端口),49152-65535(临时端口,这就是为什么代码中的端口号为49152)。

    对于多字节整数在内存中有两种存储方式:一种是低字节在前,高字节在后,这样的存储顺序被称为低端字节序(little-endian);高字节在前,低字节在后的存储顺序则被称为高端字节序(big-endian)。网络协议在处理多字节整数时,采用的是高端字节序,而不同的主机可能采用不同的字节序。因此在编程时一定要考虑主机字节序与网络字节序间的相互转换。这就是程序中使用htons函数的原因,它返回网络字节序的整数。

    (2)典型的TCP客户代码:

    ... ...
    int socket_fd;
    struct sockaddr_in serv_addr ;
    ... ...
    socket_fd = socket ( PF_INET, SOCK_STREAM, 0 );
    bzero( &serv_addr, sizeof(serv_addr) );
    serv_addr.sin_family = AF_INET ;  /* 指明通信协议族 */
    serv_addr.sin_port = htons( 49152 ) ;       /* 分配端口号 */
    inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;
    /* 分配地址,把点分十进制IPv4地址转化为32位二进制Ipv4地址。 */
    connect( socket_fd, (struct sockaddr*)serv_addr, sizeof( serv_addr ) ) ; /* 向服务器发起连接请求 */
    ... ...							/* 发送和接收数据 */
    ... ...

    对比两段代码可以看出,许多调用是服务器或客户机所特有的。另外,对于非面向连接的传输协议,代码还有简单些,没有连接的发起请求和接收请求部分。

    5、网络编程中的其他重要概念

    下面列出了网络编程中的其他重要概念,基本上都是给出这些概念能够实现的功能,读者在编程过程中如果需要这些功能,可查阅相关概念。

    (1)、I/O复用的概念

    I/O复用提供一种能力,这种能力使得当一个I/O条件满足时,进程能够及时得到这个信息。I/O复用一般应用在进程需要处理多个描述字的场合。它的一个优势在于,进程不是阻塞在真正的I/O调用上,而是阻塞在select()调用上,select()可以同时处理多个描述字,如果它所处理的所有描述字的I/O都没有处于准备好的状态,那么将阻塞;如果有一个或多个描述字I/O处于准备好状态,则select()不阻塞,同时会根据准备好的特定描述字采取相应的I/O操作。

    (2)、Unix通信域

    前面主要介绍的是PF_INET通信域,实现网际间的进程间通信。基于Unix通信域(调用socket时指定通信域为PF_LOCAL即可)的套接口可以实现单机之间的进程间通信。采用Unix通信域套接口有几个好处:Unix通信域套接口通常是TCP套接口速度的两倍;另一个好处是,通过Unix通信域套接口可以实现在进程间传递描述字。所有可用描述字描述的对象,如文件、管道、有名管道及套接口等,在我们以某种方式得到该对象的描述字后,都可以通过基于Unix域的套接口来实现对描述字的传递。接收进程收到的描述字值不一定与发送进程传递的值一致(描述字是特定于进程的),但是特们指向内核文件表中相同的项。

    (3)、原始套接口

    原始套接口提供一般套接口所不提供的功能:

    • 原始套接口可以读写一些用于控制的控制协议分组,如ICMPv4等,进而可实现一些特殊功能。
    • 原始套接口可以读写特殊的IPv4数据包。内核一般只处理几个特定协议字段的数据包,那么一些需要不同协议字段的数据包就需要通过原始套接口对其进行读写;
    • 通过原始套接口可以构造自己的Ipv4头部,也是比较有意思的一点。

    创建原始套接口需要root权限。

    (4)、对数据链路层的访问

    对数据链路层的访问,使得用户可以侦听本地电缆上的所有分组,而不需要使用任何特殊的硬件设备,在linux下读取数据链路层分组需要创建SOCK_PACKET类型的套接口,并需要有root权限。

    (5)、带外数据(out-of-band data)

    如果有一些重要信息要立刻通过套接口发送(不经过排队),请查阅与带外数据相关的文献。

    (6)、多播

    linux内核支持多播,但是在默认状态下,多数linux系统都关闭了对多播的支持。因此,为了实现多播,可能需要重新配置并编译内核。具体请参考[4]及[2]。

    结论:linux套接口编程的内容可以说是极大丰富,同时它涉及到许多的网络背景知识,有兴趣的读者可在[2]中找到比较系统而全面的介绍。

    至此,本专题系列(linux环境进程间通信)全部结束了。实际上,进程间通信的一般意义通常指的是消息队列、信号灯和共享内存,可以是posix的,也可以是SYS v的。本系列同时介绍了管道、有名管道、信号以及套接口等,是更为一般意义上的进程间通信机制。

    展开全文
  • Socket通信原理和实践

    万次阅读 多人点赞 2013-04-13 22:34:26
    我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那...

    我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:

    • 1、网络中进程之间如何通信?
    • 2、Socket是什么?
    • 3、socket的基本操作
      • 3.1、socket()函数
      • 3.2、bind()函数
      • 3.3、listen()、connect()函数
      • 3.4、accept()函数
      • 3.5、read()、write()函数等
      • 3.6、close()函数
    • 4、socket中TCP的三次握手建立连接详解
    • 5、socket中TCP的四次握手释放连接详解
    • 6、一个例子

    1、网络中进程之间如何通信?

    本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

    • 消息传递(管道、FIFO、消息队列)
    • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
    • 共享内存(匿名的和具名的)
    • 远程过程调用(Solaris门和Sun RPC)

    但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址可以唯一标识网络中的主机,而传输层的“协议+端口可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。

    2、什么是Socket?

    上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

    socket一词的起源

    在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

    3、socket的基本操作

    既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

    3.1、socket()函数

    int socket(int domain, int type, int protocol);

    socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

    正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

    • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    • type:指定socket类型。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等(socket的类型有哪些?)。
    • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。

    注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议

    当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。

    3.2、bind()函数

    正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    函数的三个参数分别为:

    • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
    • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是: 
      struct sockaddr_in {
          sa_family_t    sin_family; /* address family: AF_INET */
          in_port_t      sin_port;   /* port in network byte order */
          struct in_addr sin_addr;   /* internet address */
      };
      
      /* Internet address. */
      struct in_addr {
          uint32_t       s_addr;     /* address in network byte order */
      };
      ipv6对应的是: 
      struct sockaddr_in6 { 
          sa_family_t     sin6_family;   /* AF_INET6 */ 
          in_port_t       sin6_port;     /* port number */ 
          uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
          struct in6_addr sin6_addr;     /* IPv6 address */ 
          uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
      };
      
      struct in6_addr { 
          unsigned char   s6_addr[16];   /* IPv6 address */ 
      };
      Unix域对应的是: 
      #define UNIX_PATH_MAX    108
      
      struct sockaddr_un { 
          sa_family_t sun_family;               /* AF_UNIX */ 
          char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
      };
    • addrlen:对应的是地址的长度。

    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

    网络字节序与主机字节序

    主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

      a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

      b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

    网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

    所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

    3.3、listen()、connect()函数

    如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

    int listen(int sockfd, int backlog);
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

    listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

    connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

    3.4、accept()函数

    TCP服务器端依次调用socket()bind()listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

    accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

    注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

    3.5、read()、write()等函数

    万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

    • read()/write()
    • recv()/send()
    • readv()/writev()
    • recvmsg()/sendmsg()
    • recvfrom()/sendto()

    我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

           #include <unistd.h>
    
           ssize_t read(int fd, void *buf, size_t count);
           ssize_t write(int fd, const void *buf, size_t count);
    
           #include <sys/types.h>
           #include <sys/socket.h>
    
           ssize_t send(int sockfd, const void *buf, size_t len, int flags);
           ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
           ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                          const struct sockaddr *dest_addr, socklen_t addrlen);
           ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                            struct sockaddr *src_addr, socklen_t *addrlen);
    
           ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
           ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    

    read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

    write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

    其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。

    3.6、close()函数

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    #include <unistd.h>
    int close(int fd);

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

    4、socket中TCP的三次握手建立连接详解

    我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

    • 客户端向服务器发送一个SYN J
    • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
    • 客户端再想服务器发一个确认ACK K+1

    只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

    image

    图1、socket中发送的TCP三次握手

    从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

    5、socket中TCP的四次握手释放连接详解

    上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

    image

    图2、socket中发送的TCP四次握手

    图示过程如下:

    • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
    • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
    • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
    • 接收到这个FIN的源发送端TCP对它进行确认。

    这样每个方向上都有一个FIN和ACK。

    6.下面给出实现的一个实例

    首先,先给出实现的截图

    服务器端代码如下:

     

    [cpp] view plaincopy
     
    1. #include "InitSock.h"   
    2. #include <stdio.h>   
    3. #include <iostream>  
    4. using namespace std;  
    5. CInitSock initSock;     // 初始化Winsock库   
    6.   
    7. int main()   
    8. {   
    9.     // 创建套节字   
    10.     SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
    11.     //用来指定套接字使用的地址格式,通常使用AF_INET  
    12.     //指定套接字的类型,若是SOCK_DGRAM,则用的是udp不可靠传输  
    13.     //配合type参数使用,指定使用的协议类型(当指定套接字类型后,可以设置为0,因为默认为UDP或TCP)  
    14.     if(sListen == INVALID_SOCKET)   
    15.     {   
    16.         printf("Failed socket() \n");   
    17.         return 0;   
    18.     }   
    19.        
    20.     // 填充sockaddr_in结构 ,是个结构体  
    21.     /* struct sockaddr_in { 
    22.      
    23.     short sin_family;  //地址族(指定地址格式) ,设为AF_INET 
    24.     u_short sin_port; //端口号 
    25.     struct in_addr sin_addr; //IP地址 
    26.     char sin_zero[8]; //空子节,设为空 
    27.     } */  
    28.   
    29.     sockaddr_in sin;   
    30.     sin.sin_family = AF_INET;   
    31.     sin.sin_port = htons(4567);  //1024 ~ 49151:普通用户注册的端口号  
    32.     sin.sin_addr.S_un.S_addr = INADDR_ANY;   
    33.        
    34.     // 绑定这个套节字到一个本地地址   
    35.     if(::bind(sListen, (LPSOCKADDR)&sin, sizeof(sin)) == SOCKET_ERROR)   
    36.     {   
    37.         printf("Failed bind() \n");   
    38.         return 0;   
    39.     }   
    40.        
    41.     // 进入监听模式   
    42.     //2指的是,监听队列中允许保持的尚未处理的最大连接数  
    43.   
    44.     if(::listen(sListen, 2) == SOCKET_ERROR)   
    45.     {   
    46.         printf("Failed listen() \n");   
    47.         return 0;   
    48.     }   
    49.        
    50.     // 循环接受客户的连接请求   
    51.     sockaddr_in remoteAddr;    
    52.     int nAddrLen = sizeof(remoteAddr);   
    53.     SOCKET sClient = 0;   
    54.     char szText[] = " TCP Server Demo! \r\n";   
    55.     while(sClient==0)   
    56.     {   
    57.         // 接受一个新连接   
    58.         //((SOCKADDR*)&remoteAddr)一个指向sockaddr_in结构的指针,用于获取对方地址  
    59.         sClient = ::accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen);   
    60.         if(sClient == INVALID_SOCKET)   
    61.         {   
    62.             printf("Failed accept()");   
    63.         }   
    64.            
    65.            
    66.         printf("接受到一个连接:%s \r\n", inet_ntoa(remoteAddr.sin_addr));   
    67.         continue ;   
    68.     }   
    69.   
    70.     while(TRUE)   
    71.     {   
    72.         // 向客户端发送数据   
    73.         gets(szText) ;   
    74.         ::send(sClient, szText, strlen(szText), 0);   
    75.            
    76.         // 从客户端接收数据   
    77.         char buff[256] ;   
    78.         int nRecv = ::recv(sClient, buff, 256, 0);   
    79.         if(nRecv > 0)   
    80.         {   
    81.             buff[nRecv] = '\0';   
    82.             printf(" 接收到数据:%s\n", buff);   
    83.         }   
    84.        
    85.     }   
    86.   
    87.     // 关闭同客户端的连接   
    88.     ::closesocket(sClient);   
    89.            
    90.     // 关闭监听套节字   
    91.     ::closesocket(sListen);   
    92.   
    93.     return 0;   
    94. }   


    客户端代码:

     

     

    [cpp] view plaincopy
     
    1. #include "InitSock.h"   
    2. #include <stdio.h>   
    3. #include <iostream>   
    4. using namespace std;  
    5. CInitSock initSock;     // 初始化Winsock库   
    6.   
    7. int main()   
    8. {   
    9.     // 创建套节字   
    10.     SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);   
    11.     if(s == INVALID_SOCKET)   
    12.     {   
    13.         printf(" Failed socket() \n");   
    14.         return 0;   
    15.     }   
    16.        
    17.     // 也可以在这里调用bind函数绑定一个本地地址   
    18.     // 否则系统将会自动安排   
    19.        
    20.     // 填写远程地址信息   
    21.     sockaddr_in servAddr;    
    22.     servAddr.sin_family = AF_INET;   
    23.     servAddr.sin_port = htons(4567);   
    24.     // 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址   
    25.     // 如果你的计算机没有联网,直接使用127.0.0.1即可   
    26.     servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");   
    27.        
    28.     if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)   
    29.     {   
    30.         printf(" Failed connect() \n");   
    31.         return 0;   
    32.     }   
    33.        
    34.     char buff[256];   
    35.     char szText[256] ;   
    36.        
    37.     while(TRUE)   
    38.     {   
    39.         //从服务器端接收数据   
    40.         int nRecv = ::recv(s, buff, 256, 0);   
    41.         if(nRecv > 0)   
    42.         {   
    43.             buff[nRecv] = '\0';   
    44.             printf("接收到数据:%s\n", buff);   
    45.         }   
    46.   
    47.         // 向服务器端发送数据   
    48.   
    49.         gets(szText) ;   
    50.         szText[255] = '\0';   
    51.         ::send(s, szText, strlen(szText), 0) ;   
    52.            
    53.     }   
    54.        
    55.     // 关闭套节字   
    56.     ::closesocket(s);   
    57.     return 0;   
    58. }   


    封装的InitSock.h

     

     

    [cpp] view plaincopy
     
    1. #include <winsock2.h>   
    2. #include <stdlib.h>    
    3. #include <conio.h>    
    4. #include <stdio.h>    
    5.   
    6. #pragma comment(lib, "WS2_32")  // 链接到WS2_32.lib   
    7.   
    8. class CInitSock        
    9. {   
    10. public:   
    11.     CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)   
    12.     {   
    13.         // 初始化WS2_32.dll   
    14.         WSADATA wsaData;   
    15.         WORD sockVersion = MAKEWORD(minorVer, majorVer);   
    16.         if(::WSAStartup(sockVersion, &wsaData) != 0)   
    17.         {   
    18.             exit(0);   
    19.         }   
    20.     }   
    21.     ~CInitSock()   
    22.     {      
    23.         ::WSACleanup();    
    24.     }   
    25. };   

     

    展开全文
  • C++:实现socket通信(TCP/IP)实例

    万次阅读 多人点赞 2018-11-08 13:55:23
    首先声明,博主之前从来没有写过通信方面的东西,这次之所以写这个是因为项目需要,因此本文主要介绍一个使用C++语言及Socket来实现TCP/IP通信的实例,希望可以帮助入门者。 一、什么是TCP/IP? TCP提供基于IP...

           首先声明,博主之前从来没有写过通信方面的东西,这次之所以写这个是因为项目需要,因此本文主要介绍一个使用C++语言及Socket来实现TCP/IP通信的实例,希望可以帮助入门者。

    •  本教程,属于基础教程,针对入门者,如需更深入的功能,自行扩展;
    •  IP地址用于确定目标主机,端口号用于确定目标应用程序。

    一、什么是TCP/IP?

            TCP提供基于IP环境下的数据可靠性传输,事先需要进行三次握手来确保数据传输的可靠性。详细的博主不再赘述,感兴趣的朋友可以去search一下。

    二、什么是socket?    

            socket顾名思义就是套接字的意思,用于描述地址和端口,是一个通信链的句柄。应用程序通过socket向网络发出请求或者回应。

            socket编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW),前两者较常用。基于TCP的socket编程是流式套接字。

    三、client/server即C/S模式:

           TCP/IP通信中,主要是进行C/S交互。废话不多说,下面看看具体交互内容:

           服务端:建立socket,申明自身的port和IP,并绑定到socket,使用listen监听,然后不断用accept去查看是否有连接。如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket。如果不需要等待任何客户端连接,那么用closeSocket直接关闭自身的socket。

            客户端:建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。

    四、编程步骤

    1、server端

    (1)加载套接字库,创建套接字(WSAStartup()/socket());

    #include<winsock.h>
    #pragma comment(lib,"ws2_32.lib")
    
    void initialization();
    
    int main()
    {
      //创建套接字
      s_server = socket(AF_INET, SOCK_STREAM, 0);
    
     
    
    }
    
    
      void initialization() {
    	//初始化套接字库
    	WORD w_req = MAKEWORD(2, 2);//版本号
    	WSADATA wsadata;
    	int err;
    	err = WSAStartup(w_req, &wsadata);
    	if (err != 0) {
    		cout << "初始化套接字库失败!" << endl;
    	}
    	else {
    		cout << "初始化套接字库成功!" << endl;
    	}
    	//检测版本号
    	if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
    		cout << "套接字库版本号不符!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "套接字库版本正确!" << endl;
    	}
    	//填充服务端地址信息
    }

     (2)绑定套接字到一个IP地址和一个端口上(bind());

    server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(5010);

       (3)将套接字设置为监听模式等待连接请求(listen());

    //设置套接字为监听状态
    if (listen(s_server, SOMAXCONN) < 0) 
    {
    	cout << "设置监听状态失败!" << endl;
    	WSACleanup();
    }
    else 
    {
    	cout << "设置监听状态成功!" << endl;
    }
    cout << "服务端正在监听连接,请稍候...." << endl;

       (4)请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

    //接受连接请求
    len = sizeof(SOCKADDR);
    s_accept = accept(s_server, (SOCKADDR *)&accept_addr, &len);
    if (s_accept == SOCKET_ERROR) 
    {
      cout << "连接失败!" << endl;
      WSACleanup();
      return 0;
    }
      cout << "连接建立,准备接受数据" << endl;

        (5)用返回的套接字和客户端进行通信(send()/recv());

    //接收数据
    while (1)
     {
    	recv_len = recv(s_accept, recv_buf, 100, 0);
    	if (recv_len < 0)
            {
    	  cout << "接受失败!" << endl;
    	  break;
    	}
    	else 
            {
    	  cout << "客户端信息:" << recv_buf << endl;
    	}
    	cout << "请输入回复信息:";
    	cin >> send_buf;
    	send_len = send(s_accept, send_buf, 100, 0);
    	if (send_len < 0) 
            {
    	  cout << "发送失败!" << endl;
    	  break;
    	}
    }

        (6)返回,等待另一个连接请求;

      (7)关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

    //关闭套接字
    closesocket(s_server);
    closesocket(s_accept);
    //释放DLL资源
    WSACleanup();
    return 0;

         2、Client端

        (1)加载套接字库,创建套接字(WSAStartup()/socket);

    #include<winsock.h>
    #pragma comment(lib,"ws2_32.lib")
    
    void initialization();
    
    int main()
    {
    //创建套接字
    s_server = socket(AF_INET, SOCK_STREAM, 0);
    
    }
    
    void initialization() {
    	//初始化套接字库
    	WORD w_req = MAKEWORD(2, 2);//版本号
    	WSADATA wsadata;
    	int err;
    	err = WSAStartup(w_req, &wsadata);
    	if (err != 0) {
    		cout << "初始化套接字库失败!" << endl;
    	}
    	else {
    		cout << "初始化套接字库成功!" << endl;
    	}
    	//检测版本号
    	if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
    		cout << "套接字库版本号不符!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "套接字库版本正确!" << endl;
    	}
    	//填充服务端地址信息
    
    }

       (2)向服务器发出连接请求(connect());

    if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
    		cout << "服务器连接失败!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "服务器连接成功!" << endl;
    	}
    

      (3)和服务器进行通信(send()/recv());

    //发送,接收数据
    	while (1) {
    		cout << "请输入发送信息:";
    		cin >> send_buf;
    		send_len = send(s_server, send_buf, 100, 0);
    		if (send_len < 0) {
    			cout << "发送失败!" << endl;
    			break;
    		}
    		recv_len = recv(s_server, recv_buf, 100, 0);
    		if (recv_len < 0) {
    			cout << "接受失败!" << endl;
    			break;
    		}
    		else {
    			cout << "服务端信息:" << recv_buf << endl;
    		}
    
    	}

       (4)关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup())

    //关闭套接字
    	closesocket(s_server);
    	//释放DLL资源
    	WSACleanup();
    	

    五、Windows下基于VS2017实现的socket简单实例(TCP/IP)

    (1)server端代码

    #include "pch.h"
    #include<iostream>
    #include<winsock.h>
    #pragma comment(lib,"ws2_32.lib")
    using namespace std;
    void initialization();
    int main() {
    	//定义长度变量
    	int send_len = 0;
    	int recv_len = 0;
    	int len = 0;
    	//定义发送缓冲区和接受缓冲区
    	char send_buf[100];
    	char recv_buf[100];
    	//定义服务端套接字,接受请求套接字
    	SOCKET s_server;
    	SOCKET s_accept;
    	//服务端地址客户端地址
    	SOCKADDR_IN server_addr;
    	SOCKADDR_IN accept_addr;
    	initialization();
    	//填充服务端信息
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    	server_addr.sin_port = htons(5010);
    	//创建套接字
    	s_server = socket(AF_INET, SOCK_STREAM, 0);
    	if (bind(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
    		cout << "套接字绑定失败!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "套接字绑定成功!" << endl;
    	}
    	//设置套接字为监听状态
    	if (listen(s_server, SOMAXCONN) < 0) {
    		cout << "设置监听状态失败!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "设置监听状态成功!" << endl;
    	}
    	cout << "服务端正在监听连接,请稍候...." << endl;
    	//接受连接请求
    	len = sizeof(SOCKADDR);
    	s_accept = accept(s_server, (SOCKADDR *)&accept_addr, &len);
    	if (s_accept == SOCKET_ERROR) {
    		cout << "连接失败!" << endl;
    		WSACleanup();
    		return 0;
    	}
    	cout << "连接建立,准备接受数据" << endl;
    	//接收数据
    	while (1) {
    		recv_len = recv(s_accept, recv_buf, 100, 0);
    		if (recv_len < 0) {
    			cout << "接受失败!" << endl;
    			break;
    		}
    		else {
    			cout << "客户端信息:" << recv_buf << endl;
    		}
    		cout << "请输入回复信息:";
    		cin >> send_buf;
    		send_len = send(s_accept, send_buf, 100, 0);
    		if (send_len < 0) {
    			cout << "发送失败!" << endl;
    			break;
    		}
    	}
    	//关闭套接字
    	closesocket(s_server);
    	closesocket(s_accept);
    	//释放DLL资源
    	WSACleanup();
    	return 0;
    }
    void initialization() {
    	//初始化套接字库
    	WORD w_req = MAKEWORD(2, 2);//版本号
    	WSADATA wsadata;
    	int err;
    	err = WSAStartup(w_req, &wsadata);
    	if (err != 0) {
    		cout << "初始化套接字库失败!" << endl;
    	}
    	else {
    		cout << "初始化套接字库成功!" << endl;
    	}
    	//检测版本号
    	if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
    		cout << "套接字库版本号不符!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "套接字库版本正确!" << endl;
    	}
    	//填充服务端地址信息
    
    }
    

    (2)client端:

    #include "pch.h"
    #include<iostream>
    #include<winsock.h>
    #pragma comment(lib,"ws2_32.lib")
    using namespace std;
    void initialization();
    int main() {
    	//定义长度变量
    	int send_len = 0;
    	int recv_len = 0;
    	//定义发送缓冲区和接受缓冲区
    	char send_buf[100];
    	char recv_buf[100];
    	//定义服务端套接字,接受请求套接字
    	SOCKET s_server;
    	//服务端地址客户端地址
    	SOCKADDR_IN server_addr;
    	initialization();
    	//填充服务端信息
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    	server_addr.sin_port = htons(1234);
    	//创建套接字
    	s_server = socket(AF_INET, SOCK_STREAM, 0);
    	if (connect(s_server, (SOCKADDR *)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
    		cout << "服务器连接失败!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "服务器连接成功!" << endl;
    	}
    
    	//发送,接收数据
    	while (1) {
    		cout << "请输入发送信息:";
    		cin >> send_buf;
    		send_len = send(s_server, send_buf, 100, 0);
    		if (send_len < 0) {
    			cout << "发送失败!" << endl;
    			break;
    		}
    		recv_len = recv(s_server, recv_buf, 100, 0);
    		if (recv_len < 0) {
    			cout << "接受失败!" << endl;
    			break;
    		}
    		else {
    			cout << "服务端信息:" << recv_buf << endl;
    		}
    
    	}
    	//关闭套接字
    	closesocket(s_server);
    	//释放DLL资源
    	WSACleanup();
    	return 0;
    }
    void initialization() {
    	//初始化套接字库
    	WORD w_req = MAKEWORD(2, 2);//版本号
    	WSADATA wsadata;
    	int err;
    	err = WSAStartup(w_req, &wsadata);
    	if (err != 0) {
    		cout << "初始化套接字库失败!" << endl;
    	}
    	else {
    		cout << "初始化套接字库成功!" << endl;
    	}
    	//检测版本号
    	if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wHighVersion) != 2) {
    		cout << "套接字库版本号不符!" << endl;
    		WSACleanup();
    	}
    	else {
    		cout << "套接字库版本正确!" << endl;
    	}
    	//填充服务端地址信息
    
    }
    

    注:对于入门级别学习的同学一些使用指导,想要让这俩程序跑起来,如果只有一台电脑,那么只需要在一台电脑上VS中创建两个不同的控制台应用程序,然后把server和client代码分别copy到这俩新建项目的主程序中,直接运行即可。

    六、运行结果显示

    (1)server端

    (2)client端

     

    展开全文
  • 基于C/C++socket通信的后台木马程序

    万次阅读 2018-09-09 14:58:56
    基于C/C++socket通信的后台木马程序 什么是socket通信? 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。 建立网络通信连接至少要一对端口号(socket)。socket本质是...
  • Socket通信流程

    千次阅读 2019-01-10 20:56:47
    Socket 在了解Socket的通信流程之前首先得弄明白Socket是啥才行,那Socket到底是啥叻? Socket就是一组API,对TCP/IP协议进行封装的API!...Socket通信流程 在对Socket有了大致了解之后,再来理解Soc...
  • Socket通信原理

    万次阅读 多人点赞 2019-06-12 21:29:24
    Socket通信原理 对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问: 什么是TCP/IP、UDP? Socket在哪里呢? Socket是什么呢? 你会使用它们...
  • Java进阶(四十七)Socket通信

    万次阅读 2016-10-15 15:39:32
    Java进阶(四十七)Socket通信  今天讲解一个 Hello Word 级别的 Java Socket 通信的例子。具体通讯过程如下: 先启动Server端,进入一个死循环以便一直监听某端口是否有连接请求。然后运行Client端,客户端发出...
  • Python实现Socket通信的简单例子

    万次阅读 多人点赞 2018-05-20 11:17:07
    1、简述socket原理 socket又称套间字或者插口,是网络通信中必不可少的工具。有道是:“无socket,不网络”。由于socket最早在BSD Unix上使用,而Unix/Linux所奉为经典的至高哲学是“一切皆是文件”。因此socket在...
  • socket通信和websocket通信协议

    千次阅读 2018-08-10 12:04:32
    socket通信 网络上的两个程序通过一个双向的通信链接实现数据的交换,这个链接的一端称为一个socket socket通信流程图 服务器端通过创建一个socket的通信链接,然后绑定socket和端口号并监听,就可以接收来自...
  • Python Socket通信的实现

    千次阅读 多人点赞 2019-01-16 15:58:55
    Python Socket通信的实现Python Socket通信的实现一、Socket通信简介二、代码实现总结 Python Socket通信的实现 一、Socket通信简介 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一...
  • android socket通信实例程序

    千次下载 热门讨论 2012-05-16 23:01:09
    这是android socket通信的实例程序,具体请参考: http://blog.csdn.net/htttw/article/details/7574372
  • Socket通信入门小实例

    千次阅读 多人点赞 2014-07-29 20:45:51
    Socket通信入门小实例
  • Android Socket通信

    千次阅读 2019-03-19 16:18:04
    由于要和一些硬件设备对接,设备都是用的socket通信,用起来感觉没有websocket那么爽。 服务端我就不做介绍了。 如果服务端还没有完成的话大家可以找个工具模拟一下服务端,工具有很多,不过我也有用过sokit这个,...
  • Unity3D —— Socket通信(C#)

    万次阅读 多人点赞 2016-08-25 16:50:51
    前言:  在开始编写代码之前,我们首先需要明确:联网方式、联网步骤...我们这里使用的通信模式是Socket强连接的通信方式,并且使用C#作为编程语言,其实与.NET的Socket通信是一致的。   一、设计思想:  为...
  • C++ socket通信详解及注意事项

    千次阅读 多人点赞 2020-08-28 22:07:51
    Socket是什么 ...这里我把TCP服务器比作政府某一服务部门能,TCP客户端比作企业中某一部门电话,描述这一过程,恰好就像是socket通信,服务部门提供服务,企业部门申请服务。 要实现通信,首先政府
  • 虚拟机SOCKET通信

    2019-05-25 20:53:44
    2、编写socket通信程序 分为client(客户端)和server(服务器端)两部分程序 头文件为client和server程序所包含的函数库 不能缺少makefile文件(接下来会讲他的用处) 3、虚拟机运行 首先在命令行中运行m...
  • socket通信模型

    2015-03-05 22:47:28
    一、Socket通信 1、TCP协议书【面向连接】、【可靠】、【有序的】、以【字节流】的方式发送数据 2、基于TCP协议实现网络通信的类 · 客户端的Socket类 · 服务器端的ServerSocket类 二、Socket通信模型 1、如...
  • Socket通信详解

    万次阅读 2018-06-30 11:12:03
    网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socketsocket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP...
  • socket通信简介

    千次阅读 2016-04-11 10:57:18
    游戏一般是基于若网连接的,多采用socket进行服务端与客户端的通信,今天我们了解一下C# 的socket知识。1、什么是Socket socket 实际上是网络通信端点的一... 从技术观点看,也可以将socket通信理解为网络进程间的通信
  • socket通信字节流or字符流

    千次阅读 2017-02-28 22:04:24
    socket通信
  • ubuntu下socket通信

    千次阅读 2016-09-11 13:03:15
    之前的博文介绍了如何在ubuntu下实现unix domain socket通信,但只是本地的通信,虽然过程和网络通信很类似,但这里还是有必要了解下真正的socket通信 首先贴出server端的c代码 #include #include #include #...
  • QT - 创建TCP Socket通信

    万次阅读 多人点赞 2017-10-24 11:52:00
    QT创建TCP Socket通信  最近在学习QT,了解到QT可以进行SOCKET网络通信,进行学习,并建立一个简单的聊天DEMO。为了测试是否能与VS2012下的程序进行通信,在VS2012下建立一个客户端程序,进行通信测试,发现可以...
  • java socket 如何与 nodejs socket 通信,例如nodejs的socket.on()里面的第一个参数是如何识别的?我用的客户端是java写的,服务器端是nodejs写的,现在java通过socket与服务器端通信,如何用java模拟nodejs的socket...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 57,486
精华内容 22,994
关键字:

socket通信