【Cocos2d-x】使用BSD Socket与Java Socket进行网络通信

发表于2015-12-18
评论0 1.4k浏览

相关概念


套接字是支持TCP/IP网络通信的基本操作单元。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口


套接字,是支持TCP/IP网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。


非常非常简单的举例说明下:Socket=Ip address+ TCP/UDP + port。


每一个基于TCP/IP网络通讯的程序都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口、端口号,这样形成的整体就可以区别每一个套接字。


Berkeley 套接字(也称为BSD 套接字),一个应用程序接口(API,包括了一个用C语言写成的应用程序开发库),主要用于实现进程间通讯 接口实现用于TCP/IP协议。最初用于Unix系统。


如今,所有的现代操作系统都有一些源于Berkeley套接字接口的实现,它已成为连接Internet的标准接口。

使用Berkeley套接字的系统

  • Windows Sockets (Winsock) ,和Berkeley Sockets很相似,最初是为了便于移植Unix程序。
  • Java Sockets
  • Python sockets
  • Perl sockets


套接字API函数

这个列表是一个Berkeley套接字API库提供的函数或者方法的概要:

  • socket() 创建一个新的确定类型的套接字,类型用一个整型数值标识(文件描述符),并为它分配系统资源。
  • bind() 一般用于服务器端,将一个套接字与一个套接字地址结构相关联,比如,一个指定的本地端口和IP地址。
  • listen() 用于服务器端,使一个绑定的TCP套接字进入监听状态。
  • connect() 用于客户端,为一个套接字分配一个自由的本地端口号。 如果是TCP套接字的话,它会试图获得一个新的TCP连接。
  • accept() 用于服务器端。 它接受一个从远端客户端发出的创建一个新的TCP连接的接入请求,创建一个新的套接字,与该连接相应的套接字地址相关联。
  • send()和recv(),或者write()和read(),或者recvfrom()和sendto(), 用于往/从远程套接字发送和接受数据。
  • close() 用于系统释放分配给一个套接字的资源。 如果是TCP,连接会被中断。
  • gethostbyname()和gethostbyaddr() 用于解析主机名和地址。
  • select() 用于修整有如下情况的套接字列表: 准备读,准备写或者是有错误。
  • poll() 用于检查套接字的状态。 套接字可以被测试,看是否可以写入、读取或是有错误。
  • getsockopt() 用于查询指定的套接字一个特定的套接字选项的当前值。
  • setsockopt() 用于为指定的套接字设定一个特定的套接字选项。


socket()

socket() 为通讯创建一个端点,为套接字返回一个文件描述符。 socket() 有三个参数:

  • domain 为创建的套接字指定协议集。 例如:
    • AF_INET 表示IPv4网络协议
    • AF_INET6 表示IPv6
    • AF_UNIX 表示本地套接字(使用一个文件)
  • type 如下:
    • SOCK_STREAM (可靠的面向流服务或流套接字
    • SOCK_DGRAM (数据报文服务或者数据报文套接字
    • SOCK_SEQPACKET (可靠的连续数据包服务)
    • SOCK_RAW (在网络层之上的原始协议)
  • protocol 指定实际使用的传输协议。 最常见的就是IPPROTO_TCPIPPROTO_SCTPIPPROTO_UDPIPPROTO_DCCP。这些协议都在<netinet/in.h>中有详细说明。 如果该项为“0”的话,即根据选定的domain和type选择使用缺省协议。

如果发生错误,函数返回值为-1。 否则,函数会返回一个代表新分配的描述符的整数。

原型:
int socket(int domain, int type, int protocol);

bind()

bind() 为一个套接字分配地址。当使用socket()创建套接字后,只赋予其所使用的协议,并未分配地址。在接受其它主机的连接前,必须先调用bind()为套接字分配一个地址。bind()有三个参数:

  • sockfd, 表示使用bind函数的套接字描述符
  • my_addr, 指向sockaddr结构(用于表示所分配地址)的指针
  • addrlen, 用socklen_t字段指定了sockaddr结构的长度

如果发生错误,函数返回值为-1,否则为0。

原型
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

listen()

当socket和一个地址绑定之后,listen()函数会开始监听可能的连接请求。然而,这只能在有可靠数据流保证的时候使用,例如:数据类型(SOCK_STREAM, SOCK_SEQPACKET)。

listen()函数需要两个参数:

  • sockfd, 一个socket的描述符.
  • backlog, 一个决定监听队列大小的整数,当有一个连接请求到来,就会进入此监听队列,当队列满后,新的连接请求会返回错误。

一旦连接被接受,返回0表示成功,错误返回-1。

原型:

int listen(int sockfd, int backlog);

accept()

当应用程序监听来自其他主机的面对数据流的连接时,通过事件(比如Unix select()系统调用)通知它。必须用 accept()函数初始化连接。 Accept() 为每个连接创立新的套接字并从监听队列中移除这个连接。它使用如下参数:

  • sockfd,监听的套接字描述符
  • cliaddr, 指向sockaddr 结构体的指针,客户机地址信息。
  • addrlen,指向 socklen_t的指针,确定客户机地址结构体的大小 。

返回新的套接字描述符,出错返回-1。进一步的通信必须通过这个套接字。

Datagram 套接字不要求用accept()处理,因为接收方可能用监听套接字立即处理这个请求。

函数原型:
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

connect()

connect()系统调用为一个套接字设置连接,参数有文件描述符和主机地址。

某些类型的套接字是无连接的,大多数是UDP协议。对于这些套接字,连接时这样的:默认发送和接收数据的主机由给定的地址确定,可以使用 send()和 recv()。 返回-1表示出错,0表示成功。

函数原型:
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

gethostbyname() 和 gethostbyaddr()

gethostbyname() 和 gethostbyaddr()函数是用来解析主机名和地址的。可能会使用DNS服务或者本地主机上的其他解析机制(例如查询/etc/hosts)。返回一个指向 struct hostent的指针,这个结构体描述一个IP主机。函数使用如下参数:

  • name 指定主机名。例如 www.wikipedia.org
  • addr 指向 struct in_addr的指针,包含主机的地址。
  • len 给出 addr的长度,以字节为单位。
  • type 指定地址族类型 (比如 AF_INET)。

出错返回NULL指针,可以通过检查 h_errno 来确定是临时错误还是未知主机。正确则返回一个有效的 struct hostent *。

这些函数并不是伯克利套接字严格的组成部分。这些函数可能是过时了,新函数是 getaddrinfo() and getnameinfo(), 这些新函数是基于addrinfo数据结构。

函数原型:
struct hostent *gethostbyname(const char *name); struct hostent *gethostbyaddr(const void *addr, int len, int type);


shutdown_fd()

该函数用于关闭输入(读)、输出(写)。

参数:

1.how  流标记,SHUT_RDWR(输出输出流)、SHUT_RD(输入流)、SHUT_WR(输出流)


以上解释来自维基百科和百度百科。

>>点击查看更详细的解释



ServerSocket


服务端与客户端通过Socket进行交互的图示




服务端示例代码


说明:这是一个简单的服务端Socket与客户端Socket交互的示例代码,当客户端请求服务端时,会先从客户端读取数据,然后再往客户端写一段数据。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
package socket;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;

// Socket客户端简单示例代码
public class Client {
// 结束标记
public static String END_MARK = "n";
public static void main(String args[]) throws Exception {

String host = "127.0.0.1"; // 服务端ip地址
int port = 8899; // 服务端的端口号
// 创建Socket与服务端建立连接
Socket client = new Socket(host, port);

// 建立连接后就可以往服务端写数据了
Writer writer = new OutputStreamWriter(client.getOutputStream());
writer.append("Hello Server");
writer.append(END_MARK); // 结束标记
writer.flush();// 调用flush方法把缓冲区中的数据写出去
// 从服务端读数据
Reader reader = new InputStreamReader(client.getInputStream());
char chars[] = new char[64];
int len;
StringBuilder sb = new StringBuilder();
String temp;

int idx = -1;
while ((len = reader.read(chars)) != -1) {
temp = new String(chars, 0, len);
idx = temp.indexOf(END_MARK); //如果遇到结束标记则跳出循环
if ( idx != -1) {
sb.append(temp,0,idx); //添加结束标记前的字符串
break;
}
sb.append(temp);
}
System.out.println("from server: " + sb);
writer.close();
reader.close();
client.close();
}
}
 来自CODE的代码片
Client.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
package socket;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// Socket服务端简单示例代码
public class Server {

// 结束标记
public static String END_MARK = "n";
// 线程池
private static ExecutorService threadPool = null;

static {
// 线程池
threadPool = Executors.newCachedThreadPool();
}

public static void main(String args[]) throws IOException {
int port = 8899;
// 创建一个ServerSocket并监听8899端口
ServerSocket server = new ServerSocket(port);
boolean flag = true;
while (flag) {
// 等待客户端的连接,accept方法是阻塞的
final Socket socket = server.accept();
// 在子线程中处理socket的请求,然后继续等待下一个客户端的连接
threadPool.submit(new Runnable() {
@Override
public void run() {
try {
/**************************************************************************************/
/* 这里存在一个问题:客户端断开连接后,但调用socket.isCloseed()返回是还是false。具体原因还不清楚。
* 测试环境:系统(windows7),
* 服务端开发工具(Eclipse),
* 客户端开发工具(vs2012),
* 服务端实现(java)
* 客户端实现(c++,Socket实现为winsock)*/
/**************************************************************************************/
// 如果Socket未关闭&&可读&&可写
while (!socket.isClosed() && !socket.isInputShutdown()
&& !socket.isOutputShutdown()) {

// 连接建立后,从客户端读取数据
Reader reader = new InputStreamReader(socket
.getInputStream());
char chars[] = new char[64];
int len;
StringBuilder sb = new StringBuilder();
String temp;
int idx = -1;

// 读数据
while ((len = reader.read(chars)) != -1) {
temp = new String(chars, 0, len);
idx = temp.indexOf(END_MARK); // 如果遇到结束标记则跳出循环
if (idx != -1) {
sb.append(temp, 0, idx); // 添加结束标记前的字符串
break;
}
sb.append(temp);
}
System.out.println("from client: " + sb);

if (sb.length() > 0) {
// 往客户端写数据
Writer writer = new OutputStreamWriter(socket
.getOutputStream());
// 写数据
writer.append("Hello Client");
writer.append(END_MARK); // 发送结束标记
// 数据是存放在缓冲区,默认只有当缓冲区满时或在close时,数据才会真正发送出去(出于性能考虑),调用flush方法可以立刻把数据发送出去
writer.flush();// 调用flush方法把缓冲区的数据写出去
} else {
// 如果没有接收到客户端指令则跳出循环
break;
}
}
socket.close();
} catch (Exception e) {
// 这里为了方便,只是简单的把错误信息打印到控制台
e.printStackTrace();
}
}
});
}
server.close();
}
}
 来自CODE的代码片
Server.java

使用说明(需要先搭建java开发环境):把上面的代码保存为Server.java文件,使用javac命令编译成Server.class,然后使用java Server命令启动Server。对Client.java进行相同操作,运行Client,然后可以查看服务端Socket和客户端Socket的输出。


正常运行效果图:


我把文件放在了socket目录下


相关异常说明


1.java.net.BindException:Address already in use: JVM_Bind

端口已经被占用了,使用用netstat –ano命令,可以查看到端口使用情况,找出占用了指定端口的进程,打开任务管理器根据进程id把占用了指定端口的进程干掉即可,或者绑定一个没有被占用的端口。


2.java.net.SocketException: Connection reset

连接断开后的读和写操作引起的异常。


3.java.net.SocketException: Software caused connection abort: socket write error

服务端在往客户端写数据过程中,客户端关闭了连接。


SocketClient


SocketClient对通用的BSD Socket进行了简单的面向对象封装,使用起来更简单。【点击下载源码】


过程遇到的两个问题:

问题一:服务端读取客户端数据时如何知道已经达到数据的结尾?

在客户端发送数据的时候添加结束标记,在服务端接收数据的时候判断结束标记,如果读到结束标记则结束数据读取操作。


问题二:在客户端(C++,window系统,使用的winsock)close了socket后,在服务端(java,window系统,Socket)调用socket的isCloseed()还是返回false。

具体原因还不清楚。


接口说明:

  1. //设置服务端套接字信息(注意:在调用send/recv等等方法之前请先调用该方法一次)  
  2. // address:服务端ip地址  
  3. // port:服务端端口号  
  4. void setServerSocket(const char* address, int port);      
  5.   
  6. // 发送数据到服务端  
  7. // buff:要发送的数据  
  8. bool send(const char* buff);  
  9.   
  10. // 从服务端读取数据  
  11. // buff:从服务端读取数据的缓冲区  
  12. // size:读取数据长度  
  13. bool recv(char* buff, int size);  
  14.   
  15. // 请求(发送和接收)  
  16. // send_buff:要发送的数据  
  17. // recv_buff:从服务端读取数据的缓冲区  
  18. // recv_seze:读取数据长度  
  19. bool request(const char* send_buff, char* recv_buff, int recv_size);  
  20.   
  21. // 重置Socket(close当前socket重新创建一个空的socket)  
  22. void reset();  
  23.   
  24. // 销毁Socket实例  
  25. void destory();  

下面是调用的示例代码。

  1. // 创建SocketClient实例  
  2. SocketClient* m_socketClient = new SocketClient("127.0.0.1",8899);  
  3.   
  4. //发送数据到服务端  
  5. m_socketClient->send("send data test");  
  6.   
  7. //从服务端读取数据  
  8. const int len = 1024;  
  9. char buff[len];  
  10. m_socketClient->recv(buff,len);  
  11. CCLOG("recv data form server: %s",buff);  



项目地址

服务端:https://coding.net/u/linchaolong/p/Java_Socket_Test/git

客户端:https://coding.net/u/linchaolong/p/SocketClient/git

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引