文章目录
1. 前言
前面我们讲述了gRPC的通讯协议, 其通讯底层还是使用的原始的套接字技术。所以我们将在本篇文章来详细讲解套接字技术。本文主要讲解以下内容:
- socket通信流程
- socket接口定义
- 实现一个一对多的通信源码,开箱即用
那么什么是套接字技术呢?套接字就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。要在主机与主机之间进行通讯,只是需要一对套接字,套接字之间的连接过程可以分为三个不走:服务器监听、客户端请求、确认连接
- 服务器监听:是服务器端绑定端口后,进入一种等待连接的状态,实时监控网络状态
- 客户端请求:是指客户端向指定的ip和端口发送连接请求
- 连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客 户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
2. Socket 通信流程
3. Socket 服务器代码
创建服务器程序server.cpp
#include <arpa/inet.h>
#include <atomic>
#include <cstring>
#include <exception>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <thread>
#include <unistd.h>
#include <vector>
std::atomic<bool> g_running{false};
void handleSignal(int signo) { throw std::runtime_error((char *)"程序退出"); }
void clientDataProcess(int client_fp, char *client_ip, int client_port) {
printf("New clientDataProcess. ip: %s port: %d\n", client_ip, client_port);
int buflen = 1024;
char *buffer = (char *)malloc(buflen);
while (g_running.load()) {
ssize_t recv_len = recv(client_fp, buffer, buflen, 0);
if (recv_len > 0) {
printf("Recv data. len = %lu, msg: %s\n", recv_len, buffer);
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
free(buffer);
close(client_fp);
}
int main(int argv, char **argc) {
signal(SIGINT, handleSignal);
int fp = socket(AF_INET, SOCK_STREAM, 0);
if (fp == -1) {
printf("套接字创建失败.\n");
return -1;
}
struct sockaddr_in addr;
const char *ip = (char *)("10.50.34.149");
int port = 8088;
addr.sin_family = AF_INET;
if (strlen(ip) > 0) {
addr.sin_addr.s_addr = inet_addr(ip);
} else {
addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY对应0.0.0,0
}
addr.sin_port = htons(port);
if (bind(fp, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
printf("绑定ip: %s, 端口%d失败.\n", ip, 8088);
return -1;
}
constexpr int max_client_count = 5;
if (listen(fp, max_client_count) == -1) {
printf("监听失败.\n");
return -1;
}
printf("服务器启动成功,ip: %s, port: %d\n", ip, port);
std::thread threads[max_client_count];
g_running.store(true);
int idx = 0;
try {
while (g_running.load()) {
struct sockaddr_in clientaddr;
socklen_t addrlen = sizeof(struct sockaddr_in);
int client_fp;
printf("开始监听客户端\n");
if ((client_fp = accept(fp, (struct sockaddr *)&clientaddr,
&addrlen)) == -1) {
printf("监听失败\n");
}
printf("接受到新的客户端\n");
char *client_ip = inet_ntoa(clientaddr.sin_addr);
int client_port = ntohs(clientaddr.sin_port);
printf("New client#%d connected. ip: %s port: %d\n", client_fp,
client_ip, client_port);
threads[idx++] = std::thread(clientDataProcess, client_fp,
client_ip, client_port);
if (idx >= max_client_count) {
break;
}
}
} catch (const std::exception &e) {
printf("接受到一个异常(%s),开始关闭服务器.\n", e.what());
}
printf("关闭服务器,释放资源\n");
g_running.store(false);
for (int i = 0; i < idx; i++) {
threads[i].join();
}
close(fp);
return 0;
}
编译
g++ -std=c++11 server.cpp -o scokserver -lpthread
4. 服务器代码接口详解
我们可以通过man
来查看接口定义
4.1 创建套接字socket()
man socket
可以看到socket 需要包含sys/types.h
和sys/socket.h
两个头文件
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain
: 套接字的作用域, 在头文件sys/socket.h
中,常用有下面几个值,更多值的说明可以参考man文档说明
AF_UNIX
或AF_LOCAL
: 本地链接,可以通过localhost
进行通信AF_INET
: 创建IPv4的通讯协议AF_INET6
: 创建IPv6的通讯协议
type
: 指定通信语义类型,通常有TCP和UDP两种通信语义
SOCK_STREAM
:TCP
流式套接字,面向连接、可靠的数据传输服务,数据无差错、无重复发送,且按发送顺序接受。文件传送协议(FTP)即使用流式套接字。SOCK_DGRAM
:UDP
报式套接字,面向无连接服务,不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。网络文件系统(NFS)使用数据报式套接字。SOCK_RAW
: 原始套接字,允许对较低层协议,如IP、ICMP进行直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
protocol
: 设置传输协议族,不同的type
语义可能有着自己特定的协议,通常设置为0
返回值
:如果出现错误则返回-1,否则返回一个文件描述符
4.2 套接字绑定(bind) IP和端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
: socket创建的文件描述符
addr
: 设定的IP地址与端口
addrlen
: 地址结构体长度,通常设置为sizeof(sockaddr)
返回值
: 成功返回0,失败返回-1
4.3 套接字设置监听(listen)
int listen(int sockfd, int backlog);
sockfd
: socket创建的文件描述符
backlog
: 表示请求连接队列的最大长度,换言之表示允许连接的client的最大数量
返回值
: 成功返回0,失败返回-1
4.4 接受(accept)客户端连接
accept
用来接受一个被监听到的地址, 默认式阻塞式的,指导监听到ip才会返回,如果想走无阻塞式,可以使用accept4
的flag进行设置
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
sockfd
: socket创建的文件描述符
addr
: 被监听到的地址族,包含IP和端口号
addrlen
: addr长度,通常设置为sizeof(socklen_t)
flags
: 设置接受属性
SOCK_NONBLOCK
: 设置无阻塞式SOCK_CLOEXEC
: 设置close-on-exec
属性,当接受到一个新地址时,close旧的套接字
返回值
: 成功返回客户端的套接字文件描述符,失败返回-1
4.5 接受客户端数据(recv)
recv
接口定义
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd
: 套接字文件描述符
buf
: 接受数据的buffer指针
len
: buffer指针的长度
flags
: 设置接受数据属性,一般设置为0,表示数据将被正常接受,并存储在缓冲区中,如果没有数据可读,recv将处于阻塞状态,或者通过设置套接字的状态返回相应错误码
MSG_CMSG_CLOEXEC
: 设置close-on-exec
属性,当接受到数据时,销毁当前套接字MSG_PEEK
: 窥视传入的数据MSG_DONTWAIT
: 设置非阻塞式接受数据MSG_OOB
:处理越界数据(OOB)数据。MSG_WAITALL
: 仅当发生以下事件之一时,接收请求才会完成:- 调用方提供的缓冲区已完全满
- 连接已关闭
- 该请求已被取消或发生错误
返回值
:读出来的字节大小
4.6 关闭(close)套接字
int close(int fd);
fd
: 要关闭的套接字文件描述符
返回值
:成功返回0, 否则返回-1
5. Socket 客户端代码
创建客户端程序client.cpp
#include <arpa/inet.h>
#include <atomic>
#include <ctime>
#include <stdio.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <stdexcept>
std::atomic<bool> g_running{false};
void handleSignal(int signo) { throw std::runtime_error((char *)"程序退出"); }
std::string getTime() {
time_t now = time(0);
char currentTime[80];
strftime(currentTime, sizeof(currentTime), "%Y-%m-%d %H:%M:%S",
localtime(&now));
return std::string(currentTime);
}
int main(int argv, char **argc) {
signal(SIGINT, handleSignal);
int fp = socket(AF_INET, SOCK_STREAM, 0);
if (fp < 0) {
printf("套接字创建失败.\n");
return -1;
}
struct sockaddr_in addr;
char *ip = (char *)("10.50.34.149");
int port = 8088;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
if (connect(fp, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
printf("服务器连接失败. ip: %s port: %d\n", ip, port);
return -1;
}
printf("连接服务器成功,ip: %s, port: %d, 开始发送数据\n", ip, port);
g_running.store(true);
int pid = getpid();
try {
while (g_running.load()) {
const std::string msg = "Hello, I'm client#" + std::to_string(pid) +
". Send time is " + getTime() + "\n";
printf("发送数据:%s\n", msg.c_str());
if (send(fp, msg.c_str(), msg.size(), 0) == -1) {
printf("发送数据失败\n");
}
sleep(3);
}
} catch (const std::exception &e) {
printf("客服端接受到一个异常(%s),开始关闭服务器.\n", e.what());
}
g_running.store(false);
printf("退出客户端\n");
close(fp);
return 0;
}
编译
g++ client.cpp -o client
6. 客户端代码接口详解
客户端套接字的创建与销毁跟服务器一样,不再过多赘叙
6.1 连接服务器(connect)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
: 套接字文件描述符
addr
: 服务器的IP地址与端口
addrlen
: 地址结构体长度,通常设置为sizeof(sockaddr)
返回值
: 成功返回0,失败返回-1
6.2 发送数据(send)
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd
: 套接字文件描述符
buf
: 发送的数据
len
: 发送的数据长度
flags
: 参考recv
的flag参数,默认设置为0
返回值
: 成功返回发送成功的比特数,否则返回-1