Socket编程

流程

  1. 服务端和客户端初始化socket,得到文件描述符;
  2. 服务端调用bind,将socket绑定在指定的IP地址和端口;
  3. 服务端调用listen,进行监听;
  4. 服务端调用accept,等待客户端连接;
  5. 客户端调用connect,向服务端的地址和端口发起连接请求;
  6. 服务端accept返回用于传输的socket的文件描述符;
  7. 客户端调用writer写入数据;服务端调用read读取数据;
  8. 客户端断开连接时,调用close,服务端read读取数据,读取到EOF,待处理完数据后,服务端调用close,表示连接关闭。

服务端建立连接

服务端首先初始化socket,然后与端口绑定,对端口进行监听,调用accept阻塞,等待客户端连接。
socket()->bind()->listen()->accept()

客户端建立连接

客户端首先初始化socket,然后与服务端连接,服务端监听成功则连接建立完成
socket()->connect()

常见问题

connect,accept发生在三次握手的哪一步?

客户端connect成功返回在第二次握手,服务端accept成功返回时在三次握手成功之后。

没有accept,能建立TCP连接吗?

可以,accept()系统调用并不参与TCP三次握手的过程,执行accept()只是为了从全连接队列中取出一条连接。

没有listen,能建立TCP连接吗?

如果服务端只bind了ip和port,没有调用listen的话,客户端对服务端发起连接建立请求,服务端会回RST报文,无法建立TCP连接。
如果两个客户端同时向对方发出请求建立连接,或者客户端自连接,可以建立TCO连接。

C++ Demo

Server

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(){
/*
创建套接字
int socket(int af, int type, int protocol);
af: 地址族,IP地址类型,常用的有 AF_INET 代表IPv4地址, AF_INET6 代表IPv6地址
type: 数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM, SOCK_STREAM 面向连接, SOCK_DGRAM 无连接
protocol: 传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP ,分别代表TCP和UDP
*/
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

/*
绑定IP和PORT

sockaddr_in结构体:
struct sockaddr_in{
sa_family_t sin_family; //地址族,就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};

in_addr结构体:
struct in_addr{
in_addr_t s_addr; //32位的IP地址
};

sockaddr_in6结构体:
struct sockaddr_in6 {
sa_family_t sin6_family; //(2)地址类型,取值为AF_INET6
in_port_t sin6_port; //(2)16位端口号
uint32_t sin6_flowinfo; //(4)IPv6流信息
struct in6_addr sin6_addr; //(4)具体的IPv6地址
uint32_t sin6_scope_id; //(4)接口范围ID
};

Linux bind函数:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
*/
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));


/*
监听请求
Linux:
int listen(int sock, int backlog);
sock: 需要进入监听状态的套接字
backlog: 请求队列的最大长度
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。
*/
listen(serv_sock, 20);

/*
接收客户端请求
Linux:
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
accept() 会返回一个新的套接字与客户端通信
*/
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size = sizeof(clnt_addr);
int clnt_sock = accept(serv_sock, (struct sockaddr*)& clnt_addr, &clnt_addr_size);

/*
向客户端发送数据
ssize_t write(int fd, const void* buf, size_t count);
fd: 文件描述符
buf: 存放要写入的数据的缓冲区首地址
count: 字节数
*/
char str[] = "Hello World!";
write(clnt_sock, str, sizeof(str));

/*
关闭套接字
*/
close(clnt_sock);
close(serv_sock);
return 0;
}

Client

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(){
/*
创建套接字
int socket(int af, int type, int protocol);
af: 地址族,IP地址类型,常用的有 AF_INET 代表IPv4地址, AF_INET6 代表IPv6地址
type: 数据传输方式,常用的有 SOCK_STREAM 和 SOCK_DGRAM, SOCK_STREAM 面向连接, SOCK_DGRAM 无连接
protocol: 传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP ,分别代表TCP和UDP
*/
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

/*
绑定IP和PORT

sockaddr_in结构体:
struct sockaddr_in{
sa_family_t sin_family; //地址族,就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};

Linux:
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
*/
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

/*
从服务端读取数据
ssize_t read(int fd, void *buf, size_t count);
*/
char buffer[40];
read(sock, buffer, sizeof(buffer) - 1); //确保缓冲区的最后一个字节保留给\0
printf("Message form server: %s\n", buffer);

/*
关闭套接字
*/
close(sock);

return 0;
}

RPC

RPC(Remote Procedure Call)远程调用协议,目标是让远程调用服务更加简单、透明。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。

grpc

Google发布的开源RPC框架,是基于HTTP2.0协议的,目前已经成为最主流的RPC框架之一。

特点

  • 跨语言使用,支持C++、Java、Go、Python等编程语言。
  • 基于IDL文件定义服务,通过proto3工具生成指定语言的数据结构、服务端接口以及客户端Stub。
  • 通信协议基于标准的HTTP/2设计,支持双向流、消息头压缩、单TCP的多路复用、服务端推送等特性。
  • 序列化支持Protocol Buffer和JSON。
  • 安装简单,扩展方便。

grpc的四种服务方法

  • 简单RPC:客户端向服务器发送请求并等待响应返回,类似于正常的函数调用。
    1
    rpc GetFeature(Point) returns (Feature) {}
  • 客户端流式RPC:客户端向服务器发送请求并获取流以读回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。
  • 服务端流式RPC:客户端写入一系列消息并将它们发送到服务器,同样使用提供的流。一旦客户端完成消息写入,它就会等待服务器读取所有消息并返回响应。
  • 双端流式RPC:双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照它们喜欢的任何顺序读取和写入。

C++ GRPC Demo

  1. helloworld.proto

    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
    syntax = "proto3";

    option java_multiple_files = true;
    option java_package = "io.grpc.examples.helloworld";
    option java_outer_classname = "HelloWorldProto";
    option objc_class_prefix = "HLW";

    package helloworld;

    /*
    定义Greeter服务和SayHello方法
    SayHello接收HelloRequest参数,返回HelloReply结果
    */
    service Greeter {
    rpc SayHello (HelloRequest) return (HelloReply) {}
    }

    /*定义HelloRequest消息类型*/
    message HelloRequest {
    string name = 1;
    }

    /*定义HelloReply消息类型*/
    message HelloReply {
    string message = 1;
    }
  2. Server.cpp

    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
    #include "helloworld.grpc.pb.h"

    class GreeterServiceImpl final : public Greeter:Service
    {
    Status SayHello(ServerContext* context, const HelloRequest* request,HelloReply* reply) override
    {
    std::string prefix("Hello ");
    reply->set_message(prefix + request->name());
    return Status::OK;
    }
    };

    void RunServer() {
    // 启动GRPC的默认健康检查服务
    grpc::EnableDefaultHealthCheckService(true);
    // 注册反射机制
    grpc::reflection::InitProtoReflectionServerBuilderPlugin();

    std::string server_address("127.0.0.1:50051");

    // 创建GreeterServiceImpl实例和ServerBuilder实例
    GreeterServiceImpl service;
    ServerBuilder builder;

    // 监听端口,使用grpc::InsecureServerCredentials()进行非安全通信
    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
    // 将实例注册为可处理请求的服务
    builder.RegisterService(&service);
    // 组装并启动服务器
    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_address << std::endl;
    // 等待和关闭
    server->Wait();
    }
  3. Server.cpp

    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
    #include "helloworld.grpc.pb.h"

    class GreeterClient {
    public:
    GreeterClient(std::shared_ptr<Channel> channel) : stub_(Greeter::NewStub(channel)) {}

    std::string SayHello(const std::string& user)
    {
    // 创建一个HelloRequest对象request,并设置name值
    HelloRequest request;
    request.set_name(user);

    // 存储Server返回值
    HelloReply reply;

    // 客户端上下文
    ClientContext context;

    // 执行RPC
    Status status = stub_->SayHello(&context, request, &reply);

    // 返回值处理
    if (status.ok()) {
    return reply.message();
    } else {
    std::cout << status.error_code() << ": " << status.error_message()
    << std::endl;
    return "RPC failed";
    }
    }

    private:
    std::unique_ptr<Greeter::Stub> stub_;
    };

    void RunClient() {
    std::string server_address = "127.0.0.1:50051";
    /*
    实例化客户端,需要传递一个channel,用于创建RPC
    grpc::InsecureChannelCredentials()表示未经身份验证
    */
    Client greeter(
    grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials()));
    std::string user("world");
    std::string reply = greeter.SayHello(user);
    std::cout << "Greeter received: " << reply << std::endl;
    }

为什么有HTTP还要使用RPC?

  • HTTP是协议,RPC是框架或者说调用方式。
  • 二者的服务发现存在区别。
  • HTTP基于TCP,TCP发送的数据包包括header和body,内容冗余;RPC定制化程度更高,性能更好。

参考文章

https://www.xiaolincoding.com/