Linux Socket API:
上图是基于TCP的客户端/服务端模式
一些标识:
PF_INET(IP协议族)、AF_INET(IP地址族)、SOCK_STREAM(用于基于流式传输的协议,比如TCP)。
一些结构:
struct in_addr:用来表示主机地址,只含有一个域,是 unsigned long in s_addr。
struct sockaddr:
struct sockaddr {
u_char sa_len;
u_short sa_family; // AF_INET
char sa_data[14]; // 这里就是14字节的IP地址和端口号
}
struct sockaddr_in:
struct sockaddr_in {
u_char sin_len;
u_short sin_family; // AF_INET
u_short sin_port; // 端口号,注意必须是网络字节顺序,使用htons函数
struct in_addr sin_addr; // IP地址,注意必须是网络字节顺序,使用htons函数
char sin_zero[8];
}
一些宏:
unsigned long int INADDR_LOOPBACK:表示 "address of this machine",即环回地址,127.0.0.1,也可以是"localhost"。
unsigned long int INADDR_ANY:表示 "any incoming address",当你在设置 struct sockaddr_in 结构体中的变量 sin_addr时可以使用,表示服务器接受所有的连接请求。
unsigned long int INADDR_BROADCAST:表示发送广播报文。
unsigned long int INADDR_NONE:作为返回值,表示某些函数出错了。
一些函数:
位于 'arpa/inet.h'中:
int inet_aton(const char *name, struct in_addr *addr):将字符串表示的IP地址如'127.0.0.1'转换为二进制数据并保存在addr中,返回非0值表示成功,返回0表示失败。
unsigned long int inet_addr(const char *name):将字符串表示的IP地址转换为二进制数据后返回,如果输入不合法,会返回INADDR_NONE。
char *inet_ntoa(struct int_addr addr):将IP地址addr转换为字符串形式并返回
一些linux socket函数:
int socket(int domain, int type, int protocol); domain 是 AF_INET (准确来说在 socket() 中应该使用PF_INET,而在结构体 sockaddr_in 中使用AF_INET);type 选择 SOCK_STREAM,因为我们使用的是TCP协议, protocol 设置为0就好,socket()函数会根据 type 自动选择正确的协议。返回值是 sockfd,即 socket file discriptor,类似于文件描述符。该函数创建了一个socket。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen); sockfd 就是调用 socke 函数的返回值,my_addr 是 struct sockaddr 类型的,但在实际中,我们一般会使用 sockaddr_in 进行设置,然后再强制类型转换。my_addr 主要是设置里面的端口号和IP地址,注意字节顺序的转换即可。 addrlen 我们一般使用 sizeof(struct sockaddr)即可。该函数将一个socket绑定到指定的 ip:port。
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);这里 serv_addr 是我们要连接到的服务端的地址信息。该函数使得 sockfd 指定的 socket 连接到服务器。
int listen(int sockfd, int backlog);backlog是设定的最大连接数,即一个服务器可以接受多少个连接请求。返回-1表示出错。在调用 listen() 函数之前需要先调用 bind() 函数。
int accept(int sockfd, struct sockaddr *addr, int *addrlen);sockfd 是 listen()ing 中的 socket描述符,addr中存储某个连接的信息,即客户端的IP地址和端口。
int send(int sockfd, const void *msg, int len, int flags); 向指定的socket 发送长度为len的信息,flags 设为0就好。
int recv(int sockfd, void *buf, int len, unsigned int flags);从指定的socket接收信息并存到 buf 中,len是 buf 的最大长度。
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);
int close(int sockfd);
字节序:
htons():host to network short
ntohs():network to host short
htonl():host to network long
ntohl():network to host long
比如,我们有 struct sockaddr_in ina,ina.sin_addr.s_addr = inet_addr("127.0.0.1"),inet_addr()函数会帮我们完成到网络字节序的转变,而 ina.sin_port = htons(12345),就需要调用 htons()函数手动进行转换。
一个简单的客户端/服务端通信的例子:
客户端:
服务端:
运行结果:
基本流程可以将代码对应本文开始给出的那张示意图,对客户端来说就是依次调用 socket()、connect()、write()、read()、close()函数,对服务端来说就是依次调用 socket()、bind()、listen()、accept()、read()、write()、close()函数。
客户端首先创建一个 socket 对象,然后将其连接到一个服务器上(要求服务端必须先于客户端运行),如果连接成功,就可以发送和接受信息。
服务端也是先创建一个 socket 对象,然后调用 bind() 函数将自己绑定到服务端地址,一般就是本机所有可用地址,然后进行阻塞式监听,直到遇到客户端的连接请求,此时会返回一个维护该连接的 socket,服务端通过该新的 socket 与客户端进行通信。
Java Socket API:
在 Java 中,对客户端和服务端进行了区分,一般使用 ServerSocket 创建服务端,使用 Socket 创建客户端。
ServerSocket::
一般我常用 ServerSocket serverSocket = new ServerSocket(port);来创建一个服务端的套接字对象,我们看一下该函数的源码:
注意,该构造函数在内部调用了另外一个构造函数,其参数 port 代表端口号,backlog 代表服务器可以接受的最大连接数,bindAddr 代表服务器将使用的IP地址,为null表示本机所有的地址。所以调用该函数其实对应了 Linux Socket API 中的 socket() 、 bind()和 listen() 函数。之后通过调用 serverSocket.accept() 进行阻塞式监听,直到一个连接的到来,然后返回关于该连接的 Socket 对象。所以调用该函数其实对应了 Linux Socket API 中的 accept() 函数。
至于最后的读写我一般是用流操作,即上一步返回的 Socket 对象中的 getInputStream()函数进行读,getOutputStream()函数进行写。
Socket():
一般我常用 Socket socket = new Socket(host, port)来创建一个客户端的Socket对象,其中 host 和 port 是服务端的IP地址和端口号。调用该构造函数就对应于 Linux Socket API中的 socket() 和 connect() 函数。
一个简单的客户端/服务端通信的例子:
客户端:
服务端:
运行结果:
注意应该先启动服务端,再启动客户端。
通过比较使用 Linux Socket API 和 Java Socket API 编写的代码应该就能得到之前提出的对应关系。