Java TCP/IP Socket 编程 笔记

http://jimmee.iteye.com/blog/617110

http://jimmee.iteye.com/category/93740

一些概念:
通信信道(communication channel):将字节序列从一个主机传输到另一个主机的一种手段,可能是有线电缆,如以太网(Ethernet),也可能是无线的,如WiFi,或是其他方式的连接。

信息(information)是指由程序创建和解释的字节序列。在计算机网络环境中,这些字节序列称为分组报文(packet)。

协议(protocol)相当于相互通信的程序达成的一种约定,它规定了分组报文的交换方式和它们包含的意义。一组协议规定了分组报文的结构(例如报文中的哪一部分表明了目的地址)以及怎样对报文中所包含的信息进行解析。

TCP和UDP属于传输层,IP属于网络层,TCP,UDP和IP的具体实现通常驻留在主机的操作系统中。应用程序通过套接字API对UDP协议和TCP协议所提供的服务进行访问。

IP协议提供了一种数据报服务:每组分组报文都由网络独立处理和分发,就像信件或包裹通过邮政系统发送一样。IP报文必须包含一个保存其目的地址的字段,就像你所投递的每份包裹都写明了收件人地址一样。

TCP协议和UDP协议使用的地址叫做端口号,都是用来区分同一主机中的不同应用程序的。

客户端(client)是通信的发起者,而服务器(server)程序则被动等待客户端发起通信,并对其作出响应。

一个程序是作为客户端还是服务器,决定了它在与其对等端(peer)建立通信时使用的套接字API(客户端的对等端是服务器,反之亦然)。客服端必须首先知道服务器端的地址和端口号,反之则不需要。这个打电话类似。只要通信连接建立成功,服务器和客户端之间就没有区别了。


Socket(套接字)是一种抽象层,应用程序通过它来发送和接受数据,就像应用程序打开一个文件句柄,将数据读写到稳定的存储器上一样。一个TCP/IP套接字由一个互联网地址,一个端对端协议(TCP或UDP协议)以及一个端口号唯一确定。

 

1.InetAddress类和SocketAddress用于识别网络主机
TCP协议客户端和服务器端的套接字为Socket和ServerSocket
UDP协议的客户端和服务器端的套接字为DatagramSocket

2.
类 NetworkInterface表示一个由名称和分配给此接口的 IP 地址列表组成的网络接口,其getNetworkInterfaces()返回此机器上的所有接口。getInetAddresses()是返回一个 Enumeration 并将所有 InetAddress 或 InetAddress 的子集绑定到此网络接口的便捷方法。(注意:一个网络接口可能包含IPv4或IPv6地址)

3.类 InetAddress的getHostAddress()返回 IP 地址字符串(以文本表现形式)。 getAllByName(String host)在给定主机名的情况下,根据系统上配置的名称服务返回其 IP 地址所组成的数组。getHostName()获取此 IP 地址的主机名。getHostAddress() 返回 IP 地址字符串(以文本表现形式)。

4.TCP套接字
服务器端ServerSocket实例监听TCP链接请求,并为每个请求创建新的Socket实例。也就是说,服务器端要同时处理ServerSocket实例和Sockete实例,而客户端只需要使用Socket实例。

TCP客户端:
Java代码 复制代码  收藏代码
  1. public class TCPEchoClient {   
  2.     public static void main(String [] args) throws UnknownHostException, IOException, InterruptedException {   
  3.         if(args.length<2||args.length>3){   
  4.             throw new IllegalArgumentException("Parameter(s):<Server> <Word> [<Port>]");   
  5.         }   
  6.            
  7.         String server=args[0];   
  8.            
  9.         byte [] data=args[1].getBytes();   
  10.            
  11.         int servPort=(args.length==3)?Integer.parseInt(args[2]):7;   
  12.            
  13.         //1.创建一个Socket实例:构造函数向指定的远程主机和端口建立一个TCP连接   
  14.         Socket socket=new Socket(server,servPort);   
  15.         System.out.println("Connected to server... sending echo string");   
  16.            
  17.         /**  
  18.          *2. 通过套接字的输入输出流进行通信:一个Socket连接实例包括一个InputStream和一个OutputStream,它们的用法同于其他Java输入输出流。  
  19.          */  
  20.         InputStream in=socket.getInputStream();   
  21.         OutputStream out=socket.getOutputStream();   
  22.            
  23.         out.write(data);   
  24.            
  25.         int totalBytesRcvd=0;   
  26.         int bytesRcvd;   
  27.            
  28.         while(totalBytesRcvd<data.length){   
  29.             if((bytesRcvd=in.read(data, totalBytesRcvd, data.length-totalBytesRcvd))==-1){   
  30.                 throw new SocketException("Connection closed prematurely");   
  31.             }   
  32.             totalBytesRcvd+=bytesRcvd;   
  33.         }   
  34.         System.out.println("Receved: "+new String(data));   
  35.            
  36.         //3.使用Socet类的close()方法关闭连接   
  37.         socket.close();   
  38.     }   
  39. }  
public class TCPEchoClient {
	public static void main(String [] args) throws UnknownHostException, IOException, InterruptedException {
		if(args.length<2||args.length>3){
			throw new IllegalArgumentException("Parameter(s):<Server> <Word> [<Port>]");
		}
		
		String server=args[0];
		
		byte [] data=args[1].getBytes();
		
		int servPort=(args.length==3)?Integer.parseInt(args[2]):7;
		
		//1.创建一个Socket实例:构造函数向指定的远程主机和端口建立一个TCP连接
		Socket socket=new Socket(server,servPort);
		System.out.println("Connected to server... sending echo string");
		
		/**
		 *2. 通过套接字的输入输出流进行通信:一个Socket连接实例包括一个InputStream和一个OutputStream,它们的用法同于其他Java输入输出流。
		 */
		InputStream in=socket.getInputStream();
		OutputStream out=socket.getOutputStream();
		
		out.write(data);
		
		int totalBytesRcvd=0;
		int bytesRcvd;
		
		while(totalBytesRcvd<data.length){
			if((bytesRcvd=in.read(data, totalBytesRcvd, data.length-totalBytesRcvd))==-1){
				throw new SocketException("Connection closed prematurely");
			}
			totalBytesRcvd+=bytesRcvd;
		}
		System.out.println("Receved: "+new String(data));
		
		//3.使用Socet类的close()方法关闭连接
		socket.close();
	}
}


TCP服务器端代码:
Java代码 复制代码  收藏代码
  1. public class TCPEchoServer {   
  2.     private static final int BUFSIZE=32;   
  3.        
  4.     public static void main(String [] args) throws IOException, InterruptedException{   
  5.         if(args.length!=1){   
  6.             throw new IllegalArgumentException("Parameter(s):<Port>");   
  7.         }   
  8.            
  9.         int servPort=Integer.parseInt(args[0]);   
  10.            
  11.         //1.创建一个ServerSocket实例并制定本地端口。此套接字的功能是侦听该制定端口收到的连接。   
  12.         ServerSocket servSock=new ServerSocket(servPort);   
  13.            
  14.         int recvMsgSize;   
  15.            
  16.         byte [] receiveBuf=new byte[BUFSIZE];   
  17.            
  18.         //2.重复执行   
  19.         while(true){   
  20.             //a.调用ServerSocket的accept()方法以获取下一个客户端连接。   
  21.             //基于新建立的客户端连接,创建一个Socket实例,并由accept()方法返回   
  22.             Socket clntSock=servSock.accept();   
  23.             SocketAddress clientAddress=clntSock.getRemoteSocketAddress();   
  24.             System.out.println("Handling client at "+clientAddress);   
  25.                
  26.             //b,使用所返回的Socket实例的InputStream和OutputStream与客户端进行通信   
  27.             InputStream in=clntSock.getInputStream();   
  28.             OutputStream out=clntSock.getOutputStream();   
  29.   
  30.             while((recvMsgSize=in.read(receiveBuf))!=-1){   
  31.                 out.write(receiveBuf, 0, recvMsgSize);   
  32.             }   
  33.            
  34.             //c,通信完成后,使用Socket的close()方法关闭该客户端套接字链接   
  35.             clntSock.close();   
  36.         }   
  37.     }   
  38. }  

Java TCP/IP Socket 编程 笔记(三)—UDP的例子

 

1.UDP套接字与TCP套接字不同。UDP套接字在使用前不需要进行连接。TCP协议与电话通信相似,而UDP协议则与邮件通信相似:你寄包裹或信件时不要进行“连接”,但是你的为每个包裹和信件制定目的地址。类似地,每条信息(datagram,即数据报文)负载了自己的地址信息,并与其他信息相互独立。在接收信息时,UDP套接字扮演的角色就像是一个信箱,从不同地址发送来的信件和包裹都可以放到里面。一旦被创建,UDP套接字就可以用来连续地向不同的地址发送消息,或从任何地址接收信息。
UDP套接字将保留边界信息。UDP不像TCP一样,它是尽可能地传送消息,但并不保证信息一定能成功到达目的地址,而且信息到达的顺序与其发送顺序不一定一致(就像通过邮政部分寄信一样)。因此,UDP套接字的程序必须准备好处理信息的丢失和重排。
UDP的优点之一是效率较高,其次是灵活性。

Java通过DatagramPacket类和DatagramSocket类来使用UDP套接字。客户端和服务端都使用DatagramSocket来发送数据,使用DatagramPacket来接收数据。

发送信息时,Java程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,Java程序首先创建一个DatagramPacket类的实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。


DatagramPacket的内部有length和offset字段,如果指定了offset,数据报文的数据部分将从字节数组的指定位置发送或接收数据。length参数指定了字节数组中在发送时要传输的字节数,活在接收数据时所能接收的最多字节数。length要比data.length小,但不能比它大。

UDP客户端:

Java代码 复制代码  收藏代码
  1. import java.io.IOException;   
  2. import java.io.InterruptedIOException;   
  3. import java.net.DatagramPacket;   
  4. import java.net.DatagramSocket;   
  5. import java.net.InetAddress;   
  6.   
  7. public class UDPEchoClientTimeout {   
  8.     private static final int TIMEOUT=3000;   
  9.     private static final int MAXTRIES=5;   
  10.        
  11.     public static void main(String[] args) throws IOException {   
  12.         if(args.length<2||args.length>3){   
  13.             throw new IllegalArgumentException("Parameter(s):<Server> <Word> [<Port>]");   
  14.         }   
  15.            
  16.         InetAddress serverAddress=InetAddress.getByName(args[0]);//server address;   
  17.         byte [] bytesToSend=args[1].getBytes();   
  18.         int servPort=(args.length==3)?Integer.parseInt(args[2]):7;   
  19.            
  20.         //1.创建一个DatagramSocket实例,可以选择对本地地址和端口进行设置。    
  21.         DatagramSocket socket=new DatagramSocket();   
  22.         //设置receive()方法的最长阻塞时间   
  23.         socket.setSoTimeout(TIMEOUT);   
  24.            
  25.         DatagramPacket sendPacket=new DatagramPacket(bytesToSend,bytesToSend.length,serverAddress,servPort);   
  26.         DatagramPacket receivePacket=new DatagramPacket(new byte[bytesToSend.length],bytesToSend.length);   
  27.            
  28.         int tries=0;   
  29.         boolean receivedResponse=false;   
  30.            
  31.         do{   
  32.             //2.使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例,进行通信   
  33.             socket.send(sendPacket);   
  34.             try{   
  35.                 socket.receive(receivePacket);   
  36.                 if(!receivePacket.getAddress().equals(serverAddress)){   
  37.                     throw new IOException("Received packet from an unknown source");   
  38.                 }   
  39.                 receivedResponse=true;   
  40.             }catch(InterruptedIOException e){   
  41.                 tries+=1;   
  42.                 System.out.println("Timed out,"+(MAXTRIES-tries)+" more tries ...");   
  43.             }   
  44.                
  45.         }while(!receivedResponse&&(tries<MAXTRIES));   
  46.            
  47.            
  48.         if(receivedResponse){   
  49.             System.out.println("Received: "+new String(receivePacket.getData()));   
  50.         }else{   
  51.             System.out.println("No response -- giving up.");   
  52.         }   
  53.            
  54.         //3.通信完成后,使用DatagramSocket类的close方法来销毁该套接字   
  55.         socket.close();   
  56.     }   
  57. }  
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPEchoClientTimeout {
	private static final int TIMEOUT=3000;
	private static final int MAXTRIES=5;
	
	public static void main(String[] args) throws IOException {
		if(args.length<2||args.length>3){
			throw new IllegalArgumentException("Parameter(s):<Server> <Word> [<Port>]");
		}
		
		InetAddress serverAddress=InetAddress.getByName(args[0]);//server address;
		byte [] bytesToSend=args[1].getBytes();
		int servPort=(args.length==3)?Integer.parseInt(args[2]):7;
		
		//1.创建一个DatagramSocket实例,可以选择对本地地址和端口进行设置。 
		DatagramSocket socket=new DatagramSocket();
		//设置receive()方法的最长阻塞时间
		socket.setSoTimeout(TIMEOUT);
		
		DatagramPacket sendPacket=new DatagramPacket(bytesToSend,bytesToSend.length,serverAddress,servPort);
		DatagramPacket receivePacket=new DatagramPacket(new byte[bytesToSend.length],bytesToSend.length);
		
		int tries=0;
		boolean receivedResponse=false;
		
		do{
			//2.使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例,进行通信
			socket.send(sendPacket);
			try{
				socket.receive(receivePacket);
				if(!receivePacket.getAddress().equals(serverAddress)){
					throw new IOException("Received packet from an unknown source");
				}
				receivedResponse=true;
			}catch(InterruptedIOException e){
				tries+=1;
				System.out.println("Timed out,"+(MAXTRIES-tries)+" more tries ...");
			}
			
		}while(!receivedResponse&&(tries<MAXTRIES));
		
		
		if(receivedResponse){
			System.out.println("Received: "+new String(receivePacket.getData()));
		}else{
			System.out.println("No response -- giving up.");
		}
		
		//3.通信完成后,使用DatagramSocket类的close方法来销毁该套接字
		socket.close();
	}
}



UDP的服务器端:

Java代码 复制代码  收藏代码
  1. import java.io.IOException;   
  2. import java.net.DatagramPacket;   
  3. import java.net.DatagramSocket;   
  4.   
  5. public class UDPEchoServer {   
  6.     private static final int ECHOMAX=255;//max size of echo datagram   
  7.        
  8.     public static void main(String[] args) throws IOException {   
  9.         if(args.length!=1){   
  10.             throw new IllegalArgumentException("Parameter(s):<Port>");   
  11.         }   
  12.            
  13.         int servPort=Integer.parseInt(args[0]);   
  14.            
  15.         //1.创建一个DatagramSocket实例,指定本地端口号,可以选择指定本地地址   
  16.         DatagramSocket socket=new DatagramSocket(servPort);   
  17.         DatagramPacket packet=new DatagramPacket(new byte[ECHOMAX],ECHOMAX);   
  18.            
  19.         while(true){   
  20.             //2.使用DatagramSocket的receive方法来接收一个DatagramPacket实例。   
  21.             socket.receive(packet);   
  22.             System.out.println("Handling client at "+packet.getAddress().getHostAddress()+" on port "+packet.getPort());   
  23.                
  24.             socket.send(packet);   
  25.             packet.setLength(ECHOMAX);   
  26.         }   
  27.     }   
  28. }  
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPEchoServer {
	private static final int ECHOMAX=255;//max size of echo datagram
	
	public static void main(String[] args) throws IOException {
		if(args.length!=1){
			throw new IllegalArgumentException("Parameter(s):<Port>");
		}
		
		int servPort=Integer.parseInt(args[0]);
		
		//1.创建一个DatagramSocket实例,指定本地端口号,可以选择指定本地地址
		DatagramSocket socket=new DatagramSocket(servPort);
		DatagramPacket packet=new DatagramPacket(new byte[ECHOMAX],ECHOMAX);
		
		while(true){
			//2.使用DatagramSocket的receive方法来接收一个DatagramPacket实例。
			socket.receive(packet);
			System.out.println("Handling client at "+packet.getAddress().getHostAddress()+" on port "+packet.getPort());
			
			socket.send(packet);
			packet.setLength(ECHOMAX);
		}
	}
}



注意:DatagramPacket的getData()方法的使用,它返回数据缓冲区,是一个字节数组,需要注意。
  packet.setData(buf, offset,length);设置了接收数据时放到缓存去buf中的位置
,因此接收的数据new String(packet.getData(),packet.getOffset(),packet.getLength())的方式构造的。

 

1.TCP/IP协议要求信息必须在块(chunk)中发送和接收,而块的长度必须是8位的倍数,因此,我们可以认为TCP/IP协议中传输的信息是字节序列。如何发送和解析信息需要一定的应用程序协议。
2.信息编码:
    首先是Java里对基本整型的处理,发送时,要注意:1)每种数据类型的字节个数;2)这些字节的发送顺序是怎样的?(little-endian还是big-endian);3)所传输的数值是有符号的(signed)还是无符号的(unsigned)。具体编码时采用位操作(移位和屏蔽)就可以了。具体在Java里,可以采用DataOutputStream类和ByteArrayOutputStream来实现。恢复时可以采用DataInputStream类和ByteArrayInputStream类。
    其次,字符串和文本,在一组符号与一组整数之间的映射称为编码字符集(coded character set)。发送者与接收者必须在符号与整数的映射方式上达成共识,才能使用文本信息进行通信,最简单的方法就是定义一个标准字符集。具体编码时采用String的getBytes()方法。
    最后,位操作。如果设置一个特定的设为1,先设置好掩码(mask),之后用或操作;要清空特定一位,用与操作。
3.成帧与解析
成帧(framing)技术解决了接收端如何定位消息的首位位置的问题。
如果接收者试图从套接字中读取比消息本身更多的字节,将可能发生以下两种情况之一:如果信道中没有其他消息,接收者将阻塞等待,同时无法处理接收到的消息;如果发送者也在等待接收端的响应消息,则会形成死锁(dealock);另一方面,如果信道中还有其他消息,则接收者会将后面消息的一部分甚至全部读到第一条消息中去,这将产生一些协议错误。因此,在使用TCP套接字时,成帧就是一个非常重要的考虑因素。
有两个技术:
1. 基于定界符(Delimiter-based):消息的结束由一个唯一的标记(unique marker)指出,即发送者在传输完数据后显式添加的一个特殊字节序列。这个特殊标记不能在传输的数据中出现。幸运的是,填充(stuffing)技术能够对消息中出现的定界符进行修改,从而使接收者不将其识别为定界符。在接收者扫描定界符时,还能识别出修改过的数据,并在输出消息中对其进行还原,从而使其与原始消息一致。
2. 显式长度(Explicit length):在变长字段或消息前附加一个固定大小的字段,用来指示该字段或消息中包含了多少字节。这种方法要确定消息长度的上限,以确定保存这个长度需要的字节数。
接口:
Java代码 复制代码  收藏代码
  1. import java.io.IOException;   
  2. import java.io.OutputStream;   
  3.   
  4. public interface Framer {   
  5.     void frameMsg(byte [] message,OutputStream out) throws IOException;   
  6.     byte [] nextMsg() throws IOException;   
  7. }  
import java.io.IOException;
import java.io.OutputStream;

public interface Framer {
	void frameMsg(byte [] message,OutputStream out) throws IOException;
	byte [] nextMsg() throws IOException;
}

定界符的方式:
Java代码 复制代码  收藏代码
  1. import java.io.ByteArrayOutputStream;   
  2. import java.io.EOFException;   
  3. import java.io.IOException;   
  4. import java.io.InputStream;   
  5. import java.io.OutputStream;   
  6.   
  7. public class DelimFramer implements Framer {   
  8.     private InputStream in;//data source;   
  9.     private static final byte DELIMTER=(byte)'\n';//message delimiter   
  10.        
  11.     public DelimFramer(InputStream in){   
  12.         this.in=in;   
  13.     }   
  14.   
  15.     @Override  
  16.     public void frameMsg(byte[] message, OutputStream out) throws IOException {   
  17.         //ensure that the message dose not contain the delimiter   
  18.         for(byte b:message){   
  19.             if(b==DELIMTER)   
  20.                 throw new IOException("Message contains delimiter");   
  21.         }   
  22.         out.write(message);   
  23.         out.write(DELIMTER);   
  24.         out.flush();   
  25.            
  26.            
  27.     }   
  28.   
  29.     @Override  
  30.     public byte[] nextMsg() throws IOException {   
  31.         ByteArrayOutputStream messageBuffer=new ByteArrayOutputStream();   
  32.         int nextByte;   
  33.            
  34.         while((nextByte=in.read())!=DELIMTER){   
  35.             if(nextByte==-1){//end of stream?   
  36.                 if(messageBuffer.size()==0){   
  37.                     return null;   
  38.                 }else{   
  39.                     throw new EOFException("Non-empty message without delimiter");   
  40.                 }   
  41.             }   
  42.             messageBuffer.write(nextByte);   
  43.         }   
  44.         return messageBuffer.toByteArray();   
  45.     }   
  46. }  
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class DelimFramer implements Framer {
	private InputStream in;//data source;
	private static final byte DELIMTER=(byte)'\n';//message delimiter
	
	public DelimFramer(InputStream in){
		this.in=in;
	}

	@Override
	public void frameMsg(byte[] message, OutputStream out) throws IOException {
		//ensure that the message dose not contain the delimiter
		for(byte b:message){
			if(b==DELIMTER)
				throw new IOException("Message contains delimiter");
		}
		out.write(message);
		out.write(DELIMTER);
		out.flush();
		
		
	}

	@Override
	public byte[] nextMsg() throws IOException {
		ByteArrayOutputStream messageBuffer=new ByteArrayOutputStream();
		int nextByte;
		
		while((nextByte=in.read())!=DELIMTER){
			if(nextByte==-1){//end of stream?
				if(messageBuffer.size()==0){
					return null;
				}else{
					throw new EOFException("Non-empty message without delimiter");
				}
			}
			messageBuffer.write(nextByte);
		}
		return messageBuffer.toByteArray();
	}
}

显式长度方法:
Java代码 复制代码  收藏代码
  1. import java.io.DataInputStream;   
  2. import java.io.EOFException;   
  3. import java.io.IOException;   
  4. import java.io.InputStream;   
  5. import java.io.OutputStream;   
  6.   
  7. public class LengthFramer implements Framer {   
  8.     public static final int MAXMESSAGELENGTH=65535;   
  9.     public static final int BYTEMASK=0xff;   
  10.     public static final int SHOTMASK=0xffff;   
  11.     public static final int BYTESHIFT=8;   
  12.        
  13.     private DataInputStream in;// wrapper for data I/O   
  14.        
  15.     public LengthFramer(InputStream in) throws IOException{   
  16.         this.in=new DataInputStream(in);   
  17.     }   
  18.   
  19.     @Override  
  20.     public void frameMsg(byte[] message, OutputStream out) throws IOException {   
  21.         if(message.length>MAXMESSAGELENGTH){   
  22.             throw new IOException("message too long");   
  23.         }   
  24.            
  25.         //write length prefix   
  26.         out.write((message.length>>BYTEMASK)&BYTEMASK);   
  27.         out.write(message.length&BYTEMASK);   
  28.         //write message   
  29.         out.write(message);   
  30.         out.flush();   
  31.     }   
  32.   
  33.     @Override  
  34.     public byte[] nextMsg() throws IOException {   
  35.         int length;   
  36.            
  37.         try{   
  38.             length=in.readUnsignedShort();   
  39.         }catch(EOFException e){   
  40.             //no (or 1 byte) message;   
  41.             return null;   
  42.         }   
  43.         //0<=length<=65535;   
  44.         byte [] msg=new byte[length];   
  45.         in.readFully(msg);//if exception,it's a framing error;   
  46.         return msg;   
  47.     }   
  48. }  

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值