package com.jadyer.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.URL; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 使用JavaSocket编写发送HTTP_POST请求的工具类 * @see 与之类似的还有一个HttpClientUtil工具类 * @see 地址为http://blog.csdn.net/jadyer/article/details/8087960 * @see 还有一个使用Java原生API编写发送HTTP_POST请求的工具类 * @see 地址为http://blog.csdn.net/jadyer/article/details/8637228 * @create Apr 4, 2013 8:37:44 PM * @author 玄玉<http://blog.csdn.net/jadyer> */ public class HTTPUtil { private HTTPUtil(){} /** * 发送HTTP_POST请求 * @see 本方法默认的连接超时和读取超时均为30秒 * @see 请求参数含有中文时,亦可直接传入本方法中,本方法内部会自动根据reqCharset参数进行<code>URLEncoder.encode()</code> * @see 解码响应正文时,默认取响应头[Content-Type=text/html; charset=GBK]字符集,若无Content-Type,则使用UTF-8解码 * @param reqURL 请求地址 * @param reqParams 请求正文数据 * @param reqCharset 请求报文的编码字符集(主要针对请求参数值含中文而言) * @return reqMsg-->HTTP请求完整报文,respMsg-->HTTP响应完整报文,respMsgHex-->HTTP响应的原始字节的十六进制表示 */ public static Map<String, String> sendPostRequest(String reqURL, Map<String, String> reqParams, String reqCharset) { StringBuilder reqData = new StringBuilder(); for (Map.Entry<String, String> entry : reqParams.entrySet()) { try { reqData.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), reqCharset)).append("&"); } catch (UnsupportedEncodingException e) { System.out.println("编码字符串[" + entry.getValue() + "]时发生异常:系统不支持该字符集[" + reqCharset + "]"); reqData.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } } if (reqData.length() > 0) { reqData.setLength(reqData.length() - 1); //删除最后一个&符号 } return sendPostRequest(reqURL, reqData.toString(), reqCharset); } /** * 发送HTTP_POST请求 * @see you can see {@link HTTPUtil#sendPostRequest(String, Map, String)} * @see 注意:若欲直接调用本方法,切记请求参数值含中文时,一定要对该参数值<code>URLEncoder.encode(value, reqCharset)</code> * @see 注意:这里只是对key=value中的'value'进行encode,而非'key='..encode完毕后,再组织成key=newValue传给本方法 */ public static Map<String, String> sendPostRequest(String reqURL, String reqData, String reqCharset){ Map<String, String> respMap = new HashMap<String, String>(); OutputStream out = null; //写 InputStream in = null; //读 Socket socket = null; //客户机 String respMsg = null; String respMsgHex = null; String respCharset = "UTF-8"; StringBuilder reqMsg = new StringBuilder(); try { URL sendURL = new URL(reqURL); String host = sendURL.getHost(); int port = sendURL.getPort()==-1 ? 80 : sendURL.getPort(); /** * 创建Socket * @see --------------------------------------------------------------------------------------------------- * @see 通过有参构造方法创建Socket对象时,客户机就已经发出了网络连接请求,连接成功则返回Socket对象,反之抛IOException * @see 客户端在连接服务器时,也要进行通讯,客户端也需要分配一个端口,这个端口在客户端程序中不曾指定 * @see 这时就由客户端操作系统自动分配一个空闲的端口,默认的是自动的连续分配 * @see 如服务器端一直运行着,而客户端不停的重复运行,就会发现默认分配的端口是连续分配的 * @see 即使客户端程序已经退出了,系统也没有立即重复使用先前的端口 * @see socket = new Socket(host, port); * @see --------------------------------------------------------------------------------------------------- * @see 不过,可以通过下面的方式显式的设定客户端的IP和Port * @see socket = new Socket(host, port, InetAddress.getByName("127.0.0.1"), 8765); * @see --------------------------------------------------------------------------------------------------- */ socket = new Socket(); /** * 设置Socket属性 */ //true表示关闭Socket的缓冲,立即发送数据..其默认值为false //若Socket的底层实现不支持TCP_NODELAY选项,则会抛出SocketException socket.setTcpNoDelay(true); //表示是否允许重用Socket所绑定的本地地址 socket.setReuseAddress(true); //表示接收数据时的等待超时时间,单位毫秒..其默认值为0,表示会无限等待,永远不会超时 //当通过Socket的输入流读数据时,如果还没有数据,就会等待 //超时后会抛出SocketTimeoutException,且抛出该异常后Socket仍然是连接的,可以尝试再次读数据 socket.setSoTimeout(30000); //表示当执行Socket.close()时,是否立即关闭底层的Socket //这里设置为当Socket关闭后,底层Socket延迟5秒后再关闭,而5秒后所有未发送完的剩余数据也会被丢弃 //默认情况下,执行Socket.close()方法,该方法会立即返回,但底层的Socket实际上并不立即关闭 //它会延迟一段时间,直到发送完所有剩余的数据,才会真正关闭Socket,断开连接 //Tips:当程序通过输出流写数据时,仅仅表示程序向网络提交了一批数据,由网络负责输送到接收方 //Tips:当程序关闭Socket,有可能这批数据还在网络上传输,还未到达接收方 //Tips:这里所说的"未发送完的剩余数据"就是指这种还在网络上传输,未被接收方接收的数据 socket.setSoLinger(true, 5); //表示发送数据的缓冲区的大小 socket.setSendBufferSize(1024); //表示接收数据的缓冲区的大小 socket.setReceiveBufferSize(1024); //表示对于长时间处于空闲状态(连接的两端没有互相传送数据)的Socket,是否要自动把它关闭,true为是 //其默认值为false,表示TCP不会监视连接是否有效,不活动的客户端可能会永久存在下去,而不会注意到服务器已经崩溃 socket.setKeepAlive(true); //表示是否支持发送一个字节的TCP紧急数据,socket.sendUrgentData(data)用于发送一个字节的TCP紧急数据 //其默认为false,即接收方收到紧急数据时不作任何处理,直接将其丢弃..若用户希望发送紧急数据,则应设其为true //设为true后,接收方会把收到的紧急数据与普通数据放在同样的队列中 socket.setOOBInline(true); //该方法用于设置服务类型,以下代码请求高可靠性和最小延迟传输服务(把0x04与0x10进行位或运算) //Socket类用4个整数表示服务类型 //0x02:低成本(二进制的倒数第二位为1) //0x04:高可靠性(二进制的倒数第三位为1) //0x08:最高吞吐量(二进制的倒数第四位为1) //0x10:最小延迟(二进制的倒数第五位为1) socket.setTrafficClass(0x04 | 0x10); //该方法用于设定连接时间,延迟,带宽的相对重要性(该方法的三个参数表示网络传输数据的3项指标) //connectionTime--该参数表示用最少时间建立连接 //latency---------该参数表示最小延迟 //bandwidth-------该参数表示最高带宽 //可以为这些参数赋予任意整数值,这些整数之间的相对大小就决定了相应参数的相对重要性 //如这里设置的就是---最高带宽最重要,其次是最小连接时间,最后是最小延迟 socket.setPerformancePreferences(2, 1, 3); /** * 连接服务端 */ //客户端的Socket构造方法请求与服务器连接时,可能要等待一段时间 //默认的Socket构造方法会一直等待下去,直到连接成功,或者出现异常 //若欲设定这个等待时间,就要像下面这样使用不带参数的Socket构造方法,单位是毫秒 //若超过下面设置的30秒等待建立连接的超时时间,则会抛出SocketTimeoutException //注意:如果超时时间设为0,则表示永远不会超时 socket.connect(new InetSocketAddress(host, port), 30000); /** * 构造HTTP请求报文 */ reqMsg.append("POST ").append(sendURL.getPath()).append(" HTTP/1.1\r\n"); reqMsg.append("Cache-Control: no-cache\r\n"); reqMsg.append("Pragma: no-cache\r\n"); reqMsg.append("User-Agent: JavaSocket/").append(System.getProperty("java.version")).append("\r\n"); reqMsg.append("Host: ").append(sendURL.getHost()).append("\r\n"); reqMsg.append("Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\r\n"); reqMsg.append("Connection: keep-alive\r\n"); reqMsg.append("Content-Type: application/x-www-form-urlencoded; charset=").append(reqCharset).append("\r\n"); reqMsg.append("Content-Length: ").append(reqData.getBytes().length).append("\r\n"); reqMsg.append("\r\n"); reqMsg.append(reqData); /** * 发送HTTP请求 */ out = socket.getOutputStream(); //这里针对getBytes()补充一下:之所以没有在该方法中指明字符集(包括上面头信息组装Content-Length的时候) //是因为传进来的请求正文里面不会含中文,而非中文的英文字母符号等等,其getBytes()无论是否指明字符集,得到的都是内容一样的字节数组 //所以更建议不要直接调用本方法,而是通过sendPostRequest(String, Map<String, String>, String)间接调用本方法 //sendPostRequest(.., Map, ..)在调用本方法前,会自动对请求参数值进行URLEncoder(注意不包括key=value中的'key=') //而该方法的第三个参数reqCharset只是为了拼装HTTP请求头信息用的,目的是告诉服务器使用哪种字符集解码HTTP请求报文里面的中文信息 out.write(reqMsg.toString().getBytes()); /** * 接收HTTP响应 */ in = socket.getInputStream(); //事实上就像JDK的API所述:Closing a ByteArrayOutputStream has no effect //查询ByteArrayOutputStream.close()的源码会发现,它没有做任何事情,所以其close()与否是无所谓的 ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); byte[] buffer = new byte[512]; int len = -1; while((len=in.read(buffer)) != -1){ //将读取到的字节写到ByteArrayOutputStream中 //所以最终ByteArrayOutputStream的字节数应该等于HTTP响应报文的整体长度,而大于HTTP响应正文的长度 bytesOut.write(buffer, 0, len); } //响应的原始字节数组 byte[] respBuffer = bytesOut.toByteArray(); respMsgHex = formatToHexStringWithASCII(respBuffer); /** * 获取Content-Type中的charset值(Content-Type: text/html; charset=GBK) */ int from = 0; int to = 0; for(int i=0; i<respBuffer.length; i++){ if((respBuffer[i]==99||respBuffer[i]==67) && (respBuffer[i+1]==111||respBuffer[i+1]==79) && (respBuffer[i+2]==110||respBuffer[i+2]==78) && (respBuffer[i+3]==116||respBuffer[i+3]==84) && (respBuffer[i+4]==101||respBuffer[i+4]==69) && (respBuffer[i+5]==110||respBuffer[i+5]==78) && (respBuffer[i+6]==116||respBuffer[i+6]==84) && respBuffer[i+7]==45 && (respBuffer[i+8]==84||respBuffer[i+8]==116) && (respBuffer[i+9]==121||respBuffer[i+9]==89) && (respBuffer[i+10]==112||respBuffer[i+10]==80) && (respBuffer[i+11]==101||respBuffer[i+11]==69)){ from = i; //既然匹配到了Content-Type,那就一定不会匹配到我们想到的\r\n,所以就直接跳到下一次循环中喽.. continue; } if(from>0 && to==0 && respBuffer[i]==13 && respBuffer[i+1]==10){ //一定要加to==0限制,因为可能存在Content-Type后面还有其它的头信息 to = i; //既然得到了你想得到的,那就不要再循环啦,徒做无用功而已 break; } } //解码HTTP响应头中的Content-Type byte[] headerByte = Arrays.copyOfRange(respBuffer, from, to); //HTTP响应头信息无中文,用啥解码都可以 String contentType = new String(headerByte); //提取charset值 if(contentType.toLowerCase().contains("charset")){ respCharset = contentType.substring(contentType.lastIndexOf("=") + 1); } /** * 解码HTTP响应的完整报文 */ respMsg = bytesOut.toString(respCharset); } catch (Exception e) { System.out.println("与[" + reqURL + "]通信遇到异常,堆栈信息如下"); e.printStackTrace(); } finally { if (null!=socket && socket.isConnected() && !socket.isClosed()) { try { //此时socket的输出流和输入流也都会被关闭 //值得注意的是:先后调用Socket的shutdownInput()和shutdownOutput()方法 //值得注意的是:仅仅关闭了输入流和输出流,并不等价于调用Socket.close()方法 //通信结束后,仍然要调用Socket.close()方法,因为只有该方法才会释放Socket占用的资源,如占用的本地端口等 socket.close(); } catch (IOException e) { System.out.println("关闭客户机Socket时发生异常,堆栈信息如下"); e.printStackTrace(); } } } respMap.put("reqMsg", reqMsg.toString()); respMap.put("respMsg", respMsg); respMap.put("respMsgHex", respMsgHex); return respMap; } /** * 通过ASCII码将十进制的字节数组格式化为十六进制字符串 * @see 该方法会将字节数组中的所有字节均格式化为字符串 * @see 使用说明详见<code>formatToHexStringWithASCII(byte[], int, int)</code>方法 */ private static String formatToHexStringWithASCII(byte[] data){ return formatToHexStringWithASCII(data, 0, data.length); } /** * 通过ASCII码将十进制的字节数组格式化为十六进制字符串 * @see 该方法常用于字符串的十六进制打印,打印时左侧为十六进制数值,右侧为对应的字符串原文 * @see 在构造右侧的字符串原文时,该方法内部使用的是平台的默认字符集,来解码byte[]数组 * @see 该方法在将字节转为十六进制时,默认使用的是<code>java.util.Locale.getDefault()</code> * @see 详见String.format(String, Object...)方法和new String(byte[], int, int)构造方法 * @param data 十进制的字节数组 * @param offset 数组下标,标记从数组的第几个字节开始格式化输出 * @param length 格式长度,其不得大于数组长度,否则抛出java.lang.ArrayIndexOutOfBoundsException * @return 格式化后的十六进制字符串 */ private static String formatToHexStringWithASCII(byte[] data, int offset, int length){ int end = offset + length; StringBuilder sb = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); sb.append("\r\n------------------------------------------------------------------------"); boolean chineseCutFlag = false; for(int i=offset; i<end; i+=16){ sb.append(String.format("\r\n%04X: ", i-offset)); //X或x表示将结果格式化为十六进制整数 sb2.setLength(0); for(int j=i; j<i+16; j++){ if(j < end){ byte b = data[j]; if(b >= 0){ //ENG ASCII sb.append(String.format("%02X ", b)); if(b<32 || b>126){ //不可见字符 sb2.append(" "); }else{ sb2.append((char)b); } }else{ //CHA ASCII if(j == i+15){ //汉字前半个字节 sb.append(String.format("%02X ", data[j])); chineseCutFlag = true; String s = new String(data, j, 2); sb2.append(s); }else if(j == i&&chineseCutFlag){ //后半个字节 sb.append(String.format("%02X ", data[j])); chineseCutFlag = false; String s = new String(data, j, 1); sb2.append(s); }else{ sb.append(String.format("%02X %02X ", data[j], data[j + 1])); String s = new String(data, j, 2); sb2.append(s); j++; } } }else{ sb.append(" "); } } sb.append("| "); sb.append(sb2.toString()); } sb.append("\r\n------------------------------------------------------------------------"); return sb.toString(); } }
下面是测试方法
public static void main(String[] args) throws Exception { Map<String, String> params = new HashMap<String, String>(); params.put("goodId", "goodId"); params.put("goodsDesc", "goodsDesc"); params.put("merUserId", "merUserId"); params.put("merExtend", "merExtend"); params.put("merReqSerial", "merReqSerial"); params.put("orderDate", new SimpleDateFormat("yyyyMMdd").format(new Date())); params.put("merReqTime", new SimpleDateFormat("HHmmss").format(new Date())); params.put("serverCallUrl", "http://blog.csdn.net/jadyer"); params.put("interfaceVersion", "1.0.0.0"); params.put("busChannel", "02"); params.put("signType", "MD5"); params.put("orderValidityUnits", "m"); //m表示分钟 params.put("orderValidityNum", "30"); //这里就是30分钟 params.put("customerType", "02"); //02--18位身份证,01--15位身份证 params.put("amount", "1"); params.put("goodsName", "Tea"); params.put("merNo", "301900100000521"); params.put("orderNo", "90020120914015860583"); params.put("customerID", "5137xxxx4811"); params.put("customerName", "李治天"); params.put("mobileNo", "135xxxx8084"); params.put("cooBankNo", "GDB_CREDIT"); params.put("creditCardNo", "6225xxxx1548"); params.put("validityYear", "17"); params.put("validityMonth", "05"); params.put("CVVNo", "695"); params.put("signMsg", "This is RequestParam sign"); Map<String, String> respMap = sendPostRequest("http://127.0.0.1/tra/trade/noCardNoPassword.htm", params, "GB18030"); System.out.println("============================================================================="); System.out.println("请求报文如下"); System.out.println(respMap.get("reqMsg")); System.out.println("============================================================================="); System.out.println("响应报文如下"); System.out.println(respMap.get("respMsg")); System.out.println("============================================================================="); System.out.println("响应十六进制如下"); System.out.println(respMap.get("respMsgHex")); System.out.println("============================================================================="); }
//控制台输出如下 //============================================================================= // 请求报文如下 // POST /tra/trade/noCardNoPassword.htm HTTP/1.1 // Cache-Control: no-cache // Pragma: no-cache // User-Agent: JavaSocket/1.6.0_33 // Host: 127.0.0.1 // Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 // Connection: keep-alive // Content-Type: application/x-www-form-urlencoded; charset=GB18030 // Content-Length: 570 // // cooBankNo=GDB_CREDIT&signType=MD5&orderValidityNum=30&amount=1&CVVNo=695&merReqSerial=merReqSerial&validityYear=17&orderValidityUnits=m&merNo=301900100000521&customerName=%C0%EE%D6%CE%CC%EC&interfaceVersion=1.0.0.0&customerType=02&orderDate=20130405&validityMonth=05&merUserId=merUserId&goodId=goodId&creditCardNo=6225xxxx1548&orderNo=90020120914015860583&signMsg=This+is+RequestParam+sign&busChannel=02&serverCallUrl=http%3A%2F%2Fblog.csdn.net%2Fjadyer&merExtend=merExtend&merReqTime=010452&goodsDesc=goodsDesc&customerID=5137xxxx4811&goodsName=Tea&mobileNo=135xxxx8084 // ============================================================================= // 响应报文如下 // HTTP/1.1 200 OK // Content-Type:text/html; charset=GBK // // amount= // charSet=GB18030 // goodsName=Tea // interfaceVersion=1.0.0.0 // merchantTime= // merNo= // orderDate= // orderNo= // signMsg=10468acce39dbd59e19ec1581eeb7177 // signType=MD5 // transRst=ILLEGAL_MERCHANT_NO // goodId=goodId // goodsDesc=goodsDesc // merUserId=merUserId // mobileNo=135xxxx8084 // merExtend=merExtend // errDis=商户签名key查询失败导致无法验签 // payJnlno= // payTime= // acountDate= // payAcountDetail= // respMode=2 // payProAmt= // payBankCode= // bankAcountNo= // bankAcountName=李治天 // remark= // ============================================================================= // 响应十六进制如下 // // ------------------------------------------------------------------------ // 0000: 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F 4B 0D | HTTP/1.1 200 OK // 0010: 0A 43 6F 6E 74 65 6E 74 2D 54 79 70 65 3A 74 65 | Content-Type:te // 0020: 78 74 2F 68 74 6D 6C 3B 20 63 68 61 72 73 65 74 | xt/html; charset // 0030: 3D 47 42 4B 0D 0A 0D 0A 61 6D 6F 75 6E 74 3D 0A | =GBK amount= // 0040: 63 68 61 72 53 65 74 3D 47 42 31 38 30 33 30 0A | charSet=GB18030 // 0050: 67 6F 6F 64 73 4E 61 6D 65 3D 54 65 61 0A 69 6E | goodsName=Tea in // 0060: 74 65 72 66 61 63 65 56 65 72 73 69 6F 6E 3D 31 | terfaceVersion=1 // 0070: 2E 30 2E 30 2E 30 0A 6D 65 72 63 68 61 6E 74 54 | .0.0.0 merchantT // 0080: 69 6D 65 3D 0A 6D 65 72 4E 6F 3D 0A 6F 72 64 65 | ime= merNo= orde // 0090: 72 44 61 74 65 3D 0A 6F 72 64 65 72 4E 6F 3D 0A | rDate= orderNo= // 00A0: 73 69 67 6E 4D 73 67 3D 31 30 34 36 38 61 63 63 | signMsg=10468acc // 00B0: 65 33 39 64 62 64 35 39 65 31 39 65 63 31 35 38 | e39dbd59e19ec158 // 00C0: 31 65 65 62 37 31 37 37 0A 73 69 67 6E 54 79 70 | 1eeb7177 signTyp // 00D0: 65 3D 4D 44 35 0A 74 72 61 6E 73 52 73 74 3D 49 | e=MD5 transRst=I // 00E0: 4C 4C 45 47 41 4C 5F 4D 45 52 43 48 41 4E 54 5F | LLEGAL_MERCHANT_ // 00F0: 4E 4F 0A 67 6F 6F 64 49 64 3D 67 6F 6F 64 49 64 | NO goodId=goodId // 0100: 0A 67 6F 6F 64 73 44 65 73 63 3D 67 6F 6F 64 73 | goodsDesc=goods // 0110: 44 65 73 63 0A 6D 65 72 55 73 65 72 49 64 3D 6D | Desc merUserId=m // 0120: 65 72 55 73 65 72 49 64 0A 6D 6F 62 69 6C 65 4E | erUserId mobileN // 0130: 6F 3D 31 33 35 78 78 78 78 38 30 38 34 0A 6D 65 | o=135xxxx8084 me // 0140: 72 45 78 74 65 6E 64 3D 6D 65 72 45 78 74 65 6E | rExtend=merExten // 0150: 64 0A 65 72 72 44 69 73 3D C9 CC BB A7 C7 A9 C3 | d errDis=商户签名 // 0160: FB 6B 65 79 B2 E9 D1 AF CA A7 B0 DC B5 BC D6 C2 | ?key查询失败导致 // 0170: CE DE B7 A8 D1 E9 C7 A9 0A 70 61 79 4A 6E 6C 6E | 无法验签 payJnln // 0180: 6F 3D 0A 70 61 79 54 69 6D 65 3D 0A 61 63 6F 75 | o= payTime= acou // 0190: 6E 74 44 61 74 65 3D 0A 70 61 79 41 63 6F 75 6E | ntDate= payAcoun // 01A0: 74 44 65 74 61 69 6C 3D 0A 72 65 73 70 4D 6F 64 | tDetail= respMod // 01B0: 65 3D 32 0A 70 61 79 50 72 6F 41 6D 74 3D 0A 70 | e=2 payProAmt= p // 01C0: 61 79 42 61 6E 6B 43 6F 64 65 3D 0A 62 61 6E 6B | ayBankCode= bank // 01D0: 41 63 6F 75 6E 74 4E 6F 3D 0A 62 61 6E 6B 41 63 | AcountNo= bankAc // 01E0: 6F 75 6E 74 4E 61 6D 65 3D C0 EE D6 CE CC EC 0A | ountName=李治天 // 01F0: 72 65 6D 61 72 6B 3D | remark= // ------------------------------------------------------------------------ // ============================================================================= //