流程概述
客户端与服务器之间的网络通信基本原理如下所示,复杂一点的架构可能会添加消息中间件。
对于服务端,通信流程如下:
1、调用socket函数创建监听socket
2、调用bind函数将socket绑定到某个IP和端口号组成的二元组上
3、调用listen函数开启监听
4、当有客户端连接请求时,调用accept函数接受连接,产生一个新的socket(与客户端通信的socket)
5、基于新产生的socket调用send或recv函数开始与客户端进行数据交流
6、通信结束后,调用close函数关闭socket
对于客户端,通信流程如下:
1、调用socket函数创建客户端socket
2、调用connect函数尝试连接服务器
3、连接成功后调用send或recv函数与服务器进行数据交流
4、通信结束后,调用close函数关闭监听socket
服务器端代码实现
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
using namespace std;
int main() {
// 创建一个监听socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
cout << " create listen socket error " << endl;
return -1;
}
// 初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {
cout << "bind listen socket error" << endl;
return -1;
}
// 启动监听
if (listen(listenfd, SOMAXCONN) == -1) {
cout << "listen error" << endl;
return -1;
}
while (true) {
// 创建一个临时的客户端socket
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// 接受客户端连接
int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);
if (clientfd != -1) {
char recvBuf[32] = {
0};
// 从客户端接受数据
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
cout << "recv data from cilent , data:" << recvBuf << endl;
// 将接收到的数据原封不动地发给客户端
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
if (ret != strlen(recvBuf)) {
cout << "send data error" << endl;
} else {
cout << "send data to client successfully, data " << recvBuf <<endl;
}
} else {
cout << "recv data error" <<endl;
}
close(clientfd);
}
}
// 关闭监听socket
close(listenfd);
return 0;
}
客户端代码实现
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"
using namespace std;
int main() {
// 创建一个socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1) {
cout << " create client socket error " << endl;
return -1;
}
// 连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
serveraddr.sin_port = htons(SERVER_PORT);
if (connect(clientfd, (struct sockaddr *)& serveraddr, sizeof(serveraddr)) == -1) {
cout << "connect socket error" << endl;
return -1;
}
// 向服务器发送数据
int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
if (ret != strlen(SEND_DATA)) {
cout << "send data error" << endl;
return -1;
} else {
cout << "send data to client successfully, data " << SEND_DATA <<endl;
}
// 从服务器拉取数据
char recvBuf[32] = {
0};
ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
cout << "recv data to client successfully, data " << recvBuf <<endl;
} else {
cout << "recv data to client error" << endl;
}
// 关闭socket
close(clientfd);
return 0;
}
函数和结构讲解
sockaddr_in和sockaddr
在讲解套接字编程函数之前,有必要对socket编程的两个不可或缺的结构体进行说明:sockaddr
和 sockaddr_in
结构如下:
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
由于历史的原因,套接字函数中(如connect,bind等)使用的参数类型大多是sockaddr类型的。而如今进行套接字编程的时候大都使用sockaddr_in进行套接字地址填充.因此,这就要求对这些函数进行调用的时候都必须要讲套接字地址结构指针进行类型强制转换,例如:
struct sockaddr_in serv;
bind(sockfd,(struct sockaddr *)&serv,sizeof(serv));
socket : 创建一个socket连接
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;
向用户提供一个套接字,即套接口描述文件字,它是一个整数,如同文件描述符一样,是内核标识一个IO结构的索引。
一般传入参数是这样的:
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
__domain:这个参数指定一个协议簇,也往往被称为协议域。系统存在许多可以的协议簇,常见有AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域。这里指网络层的协议
__type:这个参数指定一个套接口的类型,套接口可能的类型有:SOCK_STREAM
、SOCK_DGRAM
、SOCK_SEQPACKET
、SOCK_RAW
等等,它们分别表明字节流、数据报、有序分组、原始套接口。
__protocol:指定相应的传输协议,也就是诸如TCP或UDP协议等等,系统针对每一个协议簇与类型提供了一个默认的协议,我们通过把protocol设置为0来使用这个默认的值。这里指传输层的协议
返回值:socket函数返回一个套接字,即套接口描述字。如果出现错误,它返回-1,并设置errno为相应的值,用户应该检测以判断出现什么错
返回值:成功则返回0,失败返回非0
bind :绑定地址以及端口号问题
在服务器端我们有这样一段代码:
// 初始化服务器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {
cout << "bind listen socket error" << endl;
return -1;
}
函数参数解释:
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
bind的地址我们使用了宏INADDR_ANY
,这个宏定义如下:
/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)
如果应用程序不关心bind绑定的IP,则可以使用这个宏,底层的协议栈服务会自动选择一个合适的IP地址,这样在多个网卡机器上选择IP地址会变得简单。
如果只想在本机上进行访问,bind函数地址可以使用本地回环地址
如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址
如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0
网络通信的基本逻辑是客户端连接服务器,即从客户端的地址:端口 连接到 服务器的地址:端口上。
一般来说,服务器的端口号是固定的,而客户端的端口号是连接发起时由操作系统随机分配的,并且不会分配被占用的端口。端口号是一个short类型的值,其范围为0~65535.
如果将bind函数中的端口号设置为0,那么操作系统会随机为程序分配一个可用的监听端口。一般来说,服务程序不会这么做,因为服务程序是要对外服务的,必须让客户端知道确切的IP地址和端口号。
在特殊的应用中,我们也可以在客户端程序以指定的端口号连接服务器,与普通的流程相比就是在创建socket与发起connect之间多了一次bind操作:
|
|
其他相关函数可以到往期文章中查看:
socket编程常见函数使用方法