共计 5379 个字符,预计需要花费 14 分钟才能阅读完成。
首发地址:day01- 从一个根底的 socket 服务说起
教程阐明:C++ 高性能网络服务保姆级教程
本节目的
实现一个基于 socket 的 echo 服务端和客户端
服务端监听流程
第一步:应用 socket 函数创立套接字
在 linux 中,一切都是文件,所有文件都有一个 int 类型的编号,称为文件描述符。服务端和客户端通信实质是在各自机器上创立一个文件,称为 socket(套接字),而后对该 socket 文件进行读写。
在 Linux 下应用 <sys/socket.h>
头文件中 socket() 函数来创立套接字
int socket(int af, int type, int protocol);
- af: IP 地址类型; IPv4 填
AF_INET
, IPv6 填AF_INET6
- type: 数据传输方式,
SOCK_STREAM
示意流格局、面向连贯,多用于 TCP。SOCK_DGRAM
示意数据报格局、无连贯,多用于 UDP - protocol: 传输协定, IPPROTO_TCP 示意 TCP。
IPPTOTO_UDP
示意 UDP。可间接填0
, 会主动依据后面的两个参数主动推导协定类型
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
第二步:应用 bind 函数绑定套接字和监听地址
socket()函数创立出套接字后,套接字中并没有任何地址信息。须要用 bind()函数将套接字和监听的 IP 和端口绑定起来,这样当有数据到该 IP 和端口时,零碎才晓得须要交给绑定的套接字解决。
bind 函数也在 <sys/socket.h>
头文件中,原型为:
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
- sock:socket 函数返回的 socket 描述符
- addr:一个 sockaddr 构造体变量的指针,后续会开展说。
- addrlen:addr 的大小,间接通过 sizeof 失去
咱们先看看 socket 和 bind 的绑定代码,上面代码中,咱们将创立的 socket 与 ip=’127.0.0.1’,port=8888 进行绑定:
#include <sys/socket.h>
#include <netinet/in.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 用 0 填充
server_addr.sin_family = AF_INET; // 应用 IPv4 地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 具体的 IP 地址;填入 INADDR_ANY 示意 "0.0.0.0"
server_addr.sin_port = htons(8888); // 端口
// 将套接字和 IP、端口绑定
bind(server_addr, (struct sockaddr*)&server_addr, sizeof(server_addr));
能够看到,咱们应用 sockaddr_in 构造体设置要绑定的地址信息,而后再强制转换为 sockaddr 类型。这是为了让 bind 函数能适应多种协定。
struct sockaddr_in{
sa_family_t sin_family; // 地址族(Address Family),也就是地址类型
uint16_t sin_port; //16 位的端口号
struct in_addr sin_addr; //32 位 IP 地址
char sin_zero[8]; // 不应用,个别用 0 填充
};
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
};
struct sockaddr{
sa_family_t sin_family; // 地址族(Address Family),也就是地址类型
char sa_data[14]; //IP 地址和端口号
};
其中,sockaddr_in 是保留 IPv4 的构造体;sockadd_in6 是保留 IPv6 的构造体;sockaddr 是通用的构造体,通过将特定协定的构造体转换成 sockaddr,以达到 bind 可绑定多种协定的目标。
留神在设置 server_addr 的端口号时,须要应用 htons 函数将传进来的端口号转换成大端字节序
计算机硬件有两种贮存数值的形式:大端字节序和小端字节序
大端字节序指数值的高位字节存在后面(低内存地址),低位字节存在前面(高内存地址)。
小端字节序则反过来,低位字节存在后面,高位字节存在前面。计算机电路先解决低位字节,效率比拟高,因为计算都是从低位开始的。而计算机读内存数据都是从低地址往高地址读。所以,计算机的外部是小端字节序。然而,人类还是习惯读写大端字节序。除了计算机的外部解决,其余的场合比方网络传输和文件贮存,简直都是用的大端字节序。
linux 在头文件 <arpa/inet.h>
提供了 htonl/htons 用于将数值转化为网络传输应用的大端字节序贮存;对应的有 ntohl/ntohs 用于将数值从网络传输应用的大端字节序转化为计算机应用的字节序
第三步:应用 listen 函数让套接字进入监听状态
int listen(int sock, int backlog); //Linux
- backlog:示意全连贯队列的大小
半连贯队列 & 全连贯队列:咱们都晓得 tcp 的三次握手,在第一次握手时,服务端收到客户端的 SYN 后,会把这个连贯放入半连贯队列中。而后发送 ACK+SYN。在收到客户端的 ACK 回包后,握手实现,会把连贯从半连贯队列移到全连贯队列中,期待解决。
第四步:调用 accept 函数获取客户端申请
调用 listen 后,此时客户端就能够和服务端三次握手建设连贯了,但建设的连贯会被放到全连贯队列中。accept 就是从这个队列中获取客户端申请。每调用一次 accept,会从队列中获取一个客户端申请。
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
- sock:服务端监听的 socket
- addr:获取到的客户端地址信息
accpet 返回一个新的套接字,之后服务端用这个套接字与连贯对应的客户端进行通信。
在没申请进来时调用 accept 会阻塞程序,直到新的申请进来。
至此,咱们就讲完了服务端的监听流程,接下来咱们能够先调用 read 期待读入客户端发过来的数据,而后再调用 write 向客户端发送数据。再用 close 把 accept_fd 敞开,断开连接。残缺代码如下
// server.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <errno.h>
int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {printf("bind err: %s\n", strerror(errno));
close(listen_fd);
return -1;
}
if (listen(listen_fd, 2048) < 0) {printf("listen err: %s\n", strerror(errno));
close(listen_fd);
return -1;
}
struct sockaddr_in client_addr;
bzero(&client_addr, sizeof(struct sockaddr_in));
socklen_t client_addr_len = sizeof(client_addr);
int accept_fd = 0;
while((accept_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len)) > 0) {printf("get accept_fd: %d from: %s:%d\n", accept_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
char read_msg[100];
int read_num = read(accept_fd, read_msg, 100);
printf("get msg from client: %s\n", read_msg);
int write_num = write(accept_fd, read_msg, read_num);
close(accept_fd);
}
}
[C++ 小常识] 在应用 printf 打印调试信息时,因为零碎缓冲区问题,如果不加 ”\n”,有时会打印不进去字符串。
C 提供的很多函数调用产生谬误时,会将错误码赋值到一个全局 int 变量 errno 上,能够通过 strerror(errno)输出具体的报错信息
客户端建设连贯
客户端就比较简单了,创立一个 sockaddr_in
变量,填充服务端的 ip 和端口,通过 connect 调用就能够获取到一个与服务端通信的套接字。
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
各个参数的阐明和 bind()雷同,不再反复。
创立连贯后,咱们先调 write 向服务端发送数据,再调用 read 期待读入服务端发过来的数据,而后调用 close 断开连接。残缺代码如下:
// client.cpp
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <cstdio>
#include <iostream>
int main() {int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_port = htons(8888);
if (connect(sock_fd, (sockaddr*)&server_addr, sizeof(server_addr)) < 0) {printf("connect err: %s\n", strerror(errno));
return -1;
};
printf("success connect to server\n");
char input_msg[100];
// 期待输出数据
std::cin >> input_msg;
printf("input_msg: %s\n", input_msg);
int write_num = write(sock_fd, input_msg, 100);
char read_msg[100];
int read_num = read(sock_fd, read_msg, 100);
printf("get from server: %s\n", read_msg);
close(sock_fd);
}
别离编译后,咱们就失去了一个 echo 服务的服务端和客户端
~# ./server
get accept_fd: 4 from: 127.0.0.1:56716
get msg from client: abc
~# ./client
abc
input_msg: abc
get from server: abc
残缺源码已上传到 CProxy-tutorial, 欢送 fork and star!
思考题
先启动 server,而后启动一个 client,不输出数据,这个时候在另外一个终端上再启动一个 client,并在第二个 client 终端中输出数据,会产生什么呢?
如果本文对你有用,点个赞再走吧!或者关注我,我会带来更多优质的内容。