文章目录
- 一、预备知识
- 1. IP 地址
- 2.端口号
- 3. TCP 协议和 UDP 协议
- 4.网络字节序
- 二、socket 编程接口
- 0. socket 常见 API
- 1. socket 系统调用
- 2. bind 系统调用
- 3. recvfrom 系统调用
- 4. sendto 系统调用
- 5. listen 系统调用
- 6. accept 系统调用
- 7. connect 系统调用
- 三、简单的 UDP 网络程序
- 1. udp echo
- 2.加入应用逻辑 -- 执行简单的 shell 命令
- 四、简单的 TCP 网络程序
- 1.单进程版本
- 2.多进程版本
- 3.多线程版本
- 4.线程池版本
- 5. TCP 简单总结
一、预备知识
1. IP 地址
-
IP 协议有两个版本,IPv4 和 IPv6 。在文章中凡是提到 IP 协议,没有特殊说明的,默认都是指 IPv4 。
-
对于 IPv4 来说,IP 地址是一个 4 字节的32位整数。
-
我们通常也使用 “点分十进制” 的字符串表示IP地址,例如 180.101.50.172,用点分割的每一个数字表示一个字节,范围是 [0, 255] 。
-
公网 IP:通常用来唯一地标识互联网中的一台主机。
-
源 IP 和目的 IP:对一个报文来讲,回答了从哪里来到哪里去的问题,最大的意义是指导一个报文该如何进行路径选择。
2.端口号
- 端口号(port)是传输层协议的内容。它是一个 2 字节 16 位的整数,用来唯一地标识一台主机上的一个进程。
进程具有独立性,进程间通信的前提工作:先得让不同的进程看到同一份资源,这份资源在这里就是网络!
源端口号和目的端口号:描述数据是哪个进程发的,要发给哪个进程。
一个进程可以关联多个端口号,但是一个端口号不可以关联多个进程,这个可以由端口号的概念得出。
3. TCP 协议和 UDP 协议
我们需要先对 TCP 协议和 UDP 协议有一个直观的认识,后面再详细讨论。
-
TCP(Transmission Control Protocol,传输控制协议)
① 传输层协议。
② 有连接。
③ 可靠传输。
④ 面向字节流。 -
UDP(User Datagram Protocol,用户数据报协议)
① 传输层协议。
② 无连接。
③ 不可靠传输。
④ 面向数据报。
TCP 的可靠和 UDP 的不可靠都是中性词,客观的,没有谁好谁不好,只有谁更合适。
4.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端和小端之分。网络数据流同样有大端和小端之分,那么为了避免网络通信中不同主机大小端不一致的问题,应如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
- 接收主机把从网络上接收到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
- 因此,网络数据流的地址应该这样规定:先发出的数据是低地址,后发出的数据是高地址。
- TCP/IP 协议规定,网络数据流应采用大端字节序,即低地址高字节。
- 不管这台主机是大端机还是小端机,都会按照这个 TCP/IP 规定的网络字节序来发送/接收数据。如果当前发送主机是小端机,就需要先将数据转成大端,否则就忽略,直接发送即可。
为了使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:
以上函数的作用:
① 如果主机是小端字节序,这些函数将参数转换为大端字节序,然后返回。
② 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
注:这些函数名很好记,h 表示 host ,n 表示 network ,l 表示 32 位长整数,s 表示 16 位短整数。
举个例子:htonl
函数表示将 32 位的长整数从主机字节序转换为网络字节序。
二、socket 编程接口
0. socket 常见 API
网络通信的标准方式有很多种,比如基于 IP 的网络通信(它对应的通信协议家族是 AF_INET,网络套接字),还有原始套接字、域间套接字。有很多种类的套接字,其实就是编程接口。这几种编程接口都是各自不同的体系,于是就会有不同套的编程接口,这样就会很麻烦,因此,干脆把不同套的编程接口统一为同一套编程接口,也就是下面的这一套。换言之,要使用不同种类的通信方式,只需要改变传入的参数即可。
// 创建 socket 文件描述符 (客户端 + 服务器, TCP/UDP)
int socket(int domain, int type, int protocol);
// 绑定端口号 (服务器, TCP/UDP)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 设置socket文件状态为监听状态 (服务器, TCP)
int listen(int sockfd, int backlog);
// 接受连接 (服务器, TCP)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 发起连接 (客户端, TCP)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
于是,为了支持不同种类的通信方式,struct sockaddr
的结构就被设计出来了,它是一种通用结构。
IPv4 地址使用的结构体是
struct sockaddr_in
,我们后面经常用到它,其定义为:
socket API 的参数都用struct sockaddr *
类型表示,在使用时传入各种类型的struct sockaddr
指针,强转成struct sockaddr *
即可。
这样,API 内部只要取得某种struct sockaddr
的首地址,不需要知道具体类型,就可以根据地址类型字段确定结构体中的内容。
1. socket 系统调用
socket
的作用:为网络通信创建一个 socket 文件。
socket
的参数:
① domain:指定协议家族。我们选择 AF_INET 。
② type:指定套接字类型。对于 TCP ,应选择 SOCK_DGRAM ;对于 UDP ,应选择 SOCK_STREAM 。
③ protocol:指定协议类型。在 TCP 和 UDP 中,我们设为 0 即可。
socket
的返回值:
① 成功,返回一个 socket 文件描述符。
② 错误,返回 -1 。
2. bind 系统调用
bind
的作用:将本地地址和一个 socket 文件进行绑定。
bind
的参数:
① sockfd:传入 socket 文件描述符。
② addr:用于指定本端的 socket 信息。
③ addrlen:用于指定本端的 socket 信息的大小。
bind
的返回值:
① 成功,返回 0 。
② 错误,返回 -1 。
3. recvfrom 系统调用
recvfrom
的作用:从一个 socket 文件接收数据。
recvfrom
的参数:
① sockfd:传入 socket 文件描述符。
② buf:用于存放读到的数据的用户层缓冲区。
③ len:用户层缓冲区的大小。
④ flags:读的方式。我们这里默认设为 0 即可。
⑤ src_addr:输入输出型参数,用于获取对端的 socket 信息。
⑥ addrlen:输入输出型参数,用于获取对端的 socket 信息的大小。
recvfrom
的返回值:
① 成功,返回接收的字节数(当对端退出时,返回 0)。
② 错误,返回 -1 。
4. sendto 系统调用
sendto
的作用:从一个 socket 文件发送数据。
sendto
的参数:
① sockfd:传入 socket 文件描述符。
② buf:用于发送数据的用户层缓冲区。
③ len:发送数据的长度。
④ flags:发送的方式。我们这里默认设为 0 即可。
⑤ dest_addr:目标对端的 socket 信息。
⑥ addrlen:目标对端的 socket 信息的大小。
sendto
的返回值:
① 成功,返回发送的字节数。
② 错误,返回 -1 。
5. listen 系统调用
listen
的作用:设置一个 socket 文件状态为监听状态,允许该 socket 文件被连接。
listen
的参数:
① sockfd:传入 socket 文件描述符。
② backlog:设置连接队列的最大长度。
listen
的返回值:
① 成功,返回 0 。
② 错误,返回 -1 。
6. accept 系统调用
accept
的作用:从一个 socket 文件接受一个连接。
accept
的参数:
① sockfd:传入处于 listen 状态的 socket 文件描述符。
② addr:输入输出型参数,用于获取对端的 socket 信息。
③ addrlen:输入输出型参数,用于获取对端的 socket 信息的大小。
accept
的返回值:
① 成功,返回一个文件描述符。
② 错误,返回 -1 。
7. connect 系统调用
connect
的作用:在一个 socket 文件上向目标发起连接。
connect
的参数:
① sockfd:传入 socket 文件描述符。
② addr:目标对端的 socket 信息。
③ addrlen:目标对端的 socket 信息的大小。
connect
的返回值:
① 成功,返回 0 。
② 错误,返回 -1 。
三、简单的 UDP 网络程序
一个服务器,必须得让客户端知道对应服务器的 socket 信息(IP + port)。
一般的服务器的 port ,必须是众所周知的,而且不能被轻易改变!
1. udp echo
程序说明:client 输入数据发送给 server ,server 接收数据后打印出来,并返回给 client 。
下面包含两个源文件:
① udp_server.cc:服务端。
② udp_client.cc:客户端。
- udp_server.cc:
// udp_server.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// const uint16_t port = 8080;
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " port" << std::endl;
}
// ./udp_server port
int main(int argc, char *argv[])
{
if(argc != 2){
Usage(argv[0]); // 参数个数不对,打印说明
return -1;
}
uint16_t port = atoi(argv[1]); // atoi:字符串转整型
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0); // UDP -> SOCK_DGRAM
if(sock < 0){
std::cerr << "socket create error: " << errno << std::endl;
return 1;
}
//2. 给该服务器绑定端口和IP(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
// 此处的端口号,是主机上的变量,是主机序列
// 由于要在网络中传送,需要转为网络字节序
//local.sin_addr.s_addr = inet_addr("xxx.xxx.xx.xxx");
// a. 需要将人识别的点分十进制的字符串风格IP地址,转换为4字节整数IP
// b. 将4字节整数IP由主机序列转换为网络序列
// in_addr_t inet_addr(const char *cp); 能完成上面ab两个工作
// 但是云服务器不允许用户直接bind公网IP
// 另外,实际正常编写的时候,我们也不会指明IP
// INADDR_ANY:不关心数据是从哪个IP上来的,只要访问的是这个端口,都会接收数据
local.sin_addr.s_addr = INADDR_ANY; // 最常用
// 原因:
// 如果bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据才会交给网络进程
// 但是,一般服务器可能有多张网卡,关联多个IP,
// 我们需要的不仅仅是某个IP上面的数据,我们需要的是所有发送到该主机该端口上的数据!
//服务器bind的本质是明确绑定的端口号,会被严格管理
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error: " << errno << std::endl;
return 2;
}
//3. 提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while(!quit) // 服务器不断提供服务,死循环
{
struct sockaddr_in peer; // 对端的socket
socklen_t len = sizeof(peer);
// 在这里,我们默认认为通信的数据是字符串
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(cnt > 0)
{
buffer[cnt] = 0; // 0 == '\0'
std::cout << "client# " << buffer << std::endl;
// 根据用户输入,构建一个新的返回字符串
std::string echo_hello = buffer;
echo_hello += "...";
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&peer, len);
}
else
{
//TODO
}
}
return 0;
}
- udp_client.cc:
// udp_client.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3){
Usage(argv[0]);
return -1;
}
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0); // UDP -> SOCK_DGRAM
if(sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 1;
}
// 客户端不需要由用户显式地绑定
// a. 首先,客户端必须也要有ip和port
// b. 但是,客户端不需要显式地bind!
// 因为一旦显式地bind,就必须明确client要和哪一个port关联
// client指明的port有可能被占用,若被占用会导致client无法使用
// server的端口会被严格地管理,跟客户端不一样
// server要的是port必须明确,而且不变,但client只要有就行!
// 一般是由OS自动给用户bind
// 当client首次对外发送数据时,OS会自动bind,采用的是随机端口的方式
//b.要给谁发
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//2. 使用服务
while(1)
{
//a.数据从哪里来?
std::string message;
std::cout << "输入# ";
std::cin >> message;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
//此处tmp就是一个“占位符”,参数不得不传
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);
if(cnt > 0)
{
// 在网络通信中,只有报文大小,或者说字节流中字节的个数
// 没有C/C++字符串这样的概念(虽然我们可能经常会遇到类似的情况)
buffer[cnt] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else{
//TODO
}
}
return 0;
}
运行测试:
2.加入应用逻辑 – 执行简单的 shell 命令
程序说明:client 输入 shell 命令发送给 server ,server 接收命令后打印出来,并返回执行命令的结果给 client 。
popen
函数的作用是执行传入的第一个参数 command ,执行完 command 后其结果会保存到一个文件,该函数会返回该文件的文件指针,第二个参数 type 表示以什么方式打开这个文件。popen
函数的底层原理是先fork
创建出子进程再pipe
实现双方通信,让父进程通过文件指针拿到结果。
pclose
函数的作用是关闭该文件。
下面包含两个源文件:
① udp_server.cc:服务端。
② udp_client.cc:客户端。
- udp_server.cc:
// udp_server.cc
#include <iostream>
#include <cstdio>
#include <string>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// const uint16_t port = 8080;
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " port" << std::endl;
}
// ./udp_server port
int main(int argc, char *argv[])
{
if(argc != 2){
Usage(argv[0]);
return -1;
}
uint16_t port = atoi(argv[1]); // atoi:字符串转整型
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0); // UDP -> SOCK_DGRAM
if(sock < 0){
std::cerr << "socket create error: " << errno << std::endl;
return 1;
}
//2. 给该服务器绑定端口和IP(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY; // 最常用
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
std::cerr << "bind error: " << errno << std::endl;
return 2;
}
//3. 提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
//Xshell
while(!quit) // 服务器不断提供服务,死循环
{
struct sockaddr_in peer; // 对端的socket
socklen_t len = sizeof(peer);
// 在这里,我们默认认为通信的数据是字符串
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(cnt > 0)
{
buffer[cnt] = 0; // 0 == '\0',可以当做一个字符串命令
FILE *fp = popen(buffer, "r");
std::string echo_string;
char line[1024] = {0}; // 使用fgets从文件一行一行地读取内容
while(fgets(line, sizeof(line), fp) != NULL){
echo_string += line;
}
// if(feof(fp)){ // 判断是否读到文件的末尾EOF
// //读取结果完成
// }
pclose(fp);
std::cout << "client# " << buffer << std::endl;
// 根据用户输入,构建一个新的返回字符串
sendto(sock, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
}
else
{
//TODO
}
}
return 0;
}
- udp_client.cc:
// udp_client.cc
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3){
Usage(argv[0]);
return -1;
}
//1. 创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0); // UDP -> SOCK_DGRAM
if(sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 1;
}
//b.要给谁发
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
//2. 使用服务
while(1)
{
// //a.数据从哪里来?
// std::string message;
// std::cout << "输入# ";
// std::cin >> message;
std::cout << "MyShell $ ";
char line[1024];
fgets(line, sizeof(line), stdin);
sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));
//此处tmp就是一个“占位符”,参数不得不传
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len);
if(cnt > 0)
{
// 在网络通信中,只有报文大小,或者说字节流中字节的个数
// 没有C/C++字符串这样的概念(虽然我们可能经常会遇到类似的情况)
buffer[cnt] = 0;
std::cout << buffer << std::endl;
}
else{
//TODO
}
}
return 0;
}
运行测试:
四、简单的 TCP 网络程序
程序说明:client 输入数据发送给 server ,server 接收数据后打印出来,并返回给 client 。
下面包含两个源文件:
① tcp_server.cc:服务端。
② tcp_client.cc:客户端。
1.单进程版本
- tcp_server.cc:
// tcp_server.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(listen_sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 2;
}
//2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 3;
}
//3. 因为tcp是面向连接的,a.在通信前,需要建立连接 b.然后才能通信
// 一定有人主动建立连接(客户端,需要服务),一定有人被动接受连接(服务器,提供服务)
// 我们当前写的是一个server,不间断地等待客户端到来
// 我们要不断地给客户端提供一个建立连接的功能
// 设置套接字是listen状态,本质是允许客户端连接
const int back_log = 5; // 先设为5,这里先不解释
if(listen(listen_sock, back_log) < 0){
std::cerr << "listen error: " << errno << std::endl;
return 4;
}
for( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)
{
continue;
}
std::cout << "get a new link..." << std::endl;
// 单进程版本,没人使用!
// 提供服务,是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0){ // 读到EOF,表明对端退出了
std::cout << "client quit..." << std::endl;
break;
}
else{
std::cerr << "read error" << std::endl;
break;
}
}
}
return 0;
}
- tcp_client.cc:
// tcp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// client无需显式地bind,client -> server
// client -> connect!
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 将一段空间清零,不推荐使用
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
//2. 发起连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect server failed!" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 进行正常的业务请求
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
运行测试:
2.多进程版本
- tcp_server.cc:
// tcp_server.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
// 提供服务,是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0){ // 读到EOF,表明对端退出了
std::cout << "client quit..." << std::endl;
break;
}
else{
std::cerr << "read error" << std::endl;
break;
}
}
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(listen_sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 2;
}
//2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 3;
}
//3. 设置套接字是listen状态,本质是允许客户端连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0){
std::cerr << "listen error: " << errno << std::endl;
return 4;
}
// 在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
signal(SIGCHLD, SIG_IGN);
for( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)
{
continue;
}
// 打印对端的socket信息
uint16_t cli_port = ntohs(peer.sin_port); // 网络序列转主机序列
std::string cli_ip = inet_ntoa(peer.sin_addr); // 作用与inet_addr函数相反
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;
// 多进程版本
pid_t id = fork();
if(id < 0){
continue;
}
else if(id == 0){ // 曾经被父进程打开的fd,会被子进程继承
// child // 无论父子进程,强烈建议关闭掉不需要的fd
close(listen_sock);
// 不设置信号的另一种做法
// if(fork() > 0) exit(0); //退出的是子进程,向后走的是孙子进程,父孙进程没关系
ServiceIO(new_sock);
close(new_sock); //如果不关闭不需要的fd,会造成fd泄漏
exit(0);
}
else{
// parent,不需要等待child
// waitpid(id, nullptr, 0); // 阻塞式等待,但几乎不阻塞,因为子进程很快退出
close(new_sock);
}
}
return 0;
}
- tcp_client.cc:
// tcp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// client无需显式地bind,client -> server
// client -> connect!
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 将一段空间清零,不推荐使用
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
//2. 发起连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect server failed!" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 进行正常的业务请求
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
运行测试:
3.多线程版本
- tcp_server.cc:
// tcp_server.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
void ServiceIO(int new_sock)
{
// 提供服务,是一个死循环
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(new_sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0; // 将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(new_sock, echo_string.c_str(), echo_string.size());
}
else if(s == 0){ // 读到EOF,表明对端退出了
std::cout << "client quit..." << std::endl;
break;
}
else{
std::cerr << "read error" << std::endl;
break;
}
}
}
void *HandlerRequest(void *args)
{
pthread_detach(pthread_self());
int sock = *(int*)args;
delete (int*)args;
ServiceIO(sock);
close(sock);
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(listen_sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 2;
}
//2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 3;
}
//3. 设置套接字是listen状态,本质是允许客户端连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0){
std::cerr << "listen error: " << errno << std::endl;
return 4;
}
for( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)
{
continue;
}
// 打印对端的socket信息
uint16_t cli_port = ntohs(peer.sin_port); // 网络序列转主机序列
std::string cli_ip = inet_ntoa(peer.sin_addr); // 作用与inet_addr函数相反
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;
// 多线程版本
// 曾经被主线程打开的fd,新线程能看到和共享
pthread_t tid;
int* pram = new int(new_sock);
pthread_create(&tid, nullptr, HandlerRequest, (void*)pram);
}
return 0;
}
- tcp_client.cc:
// tcp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// client无需显式地bind,client -> server
// client -> connect!
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 将一段空间清零,不推荐使用
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
//2. 发起连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect server failed!" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 进行正常的业务请求
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
运行测试:
4.线程池版本
上面的多进程版本和多线程版本的网络程序都是有问题的:创建进程或线程无上限,而且当客户端连接来了,我们才给客户端创建进程或线程。
实际上,最好是线程池版本。
下面包含四个文件:
① Task.hpp:任务的声明和定义。
② thread_pool.hpp:线程池的声明和定义。
③ tcp_server.cc:服务端。
④ tcp_client.cc:客户端。
- Task.hpp:
// Task.hpp
#pragma once
#include <iostream>
#include <cstring>
#include <unistd.h>
namespace ns_task
{
class Task
{
private:
int sock_;
public:
Task() : sock_(-1) {}
Task(int sock) : sock_(sock)
{
}
int Run()
{
// 实际上不应该是长服务,应该是短服务
// 相当于一请求一响应就完了
// // 提供服务,是一个死循环
// while(true)
// {
char buffer[1024];
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(sock_, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0; // 将获取的内容当成字符串
std::cout << "client# " << buffer << std::endl;
std::string echo_string = ">>>server<<<, ";
echo_string += buffer;
write(sock_, echo_string.c_str(), echo_string.size());
}
else if (s == 0)
{ // 读到EOF,表明对端退出了
std::cout << "client quit..." << std::endl;
// break;
}
else
{
std::cerr << "read error" << std::endl;
// break;
}
// }
close(sock_);
}
~Task() {}
};
}
- thread_pool.hpp:
// thread_pool.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
namespace ns_threadpool
{
const int g_num = 5;
template <class T>
class ThreadPool
{
private:
int num_;
std::queue<T> task_queue_; // 该成员是一个临界资源
pthread_mutex_t mtx_;
pthread_cond_t cond_;
static ThreadPool<T> *ins;
private:
// 构造函数必须得实现,但是必须得私有化
ThreadPool(int num = g_num) : num_(num)
{
pthread_mutex_init(&mtx_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
ThreadPool(const ThreadPool<T> &tp) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
public:
static ThreadPool<T> *GetInstance()
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
{
pthread_mutex_lock(&lock);
// 当前单例对象还没有被创建
if (ins == nullptr)
{
ins = new ThreadPool<T>();
ins->InitThreadPool();
std::cout << "首次加载对象" << std::endl;
}
pthread_mutex_unlock(&lock);
}
return ins;
}
void Lock()
{
pthread_mutex_lock(&mtx_);
}
void Unlock()
{
pthread_mutex_unlock(&mtx_);
}
void Wait()
{
pthread_cond_wait(&cond_, &mtx_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
bool IsEmpty()
{
return task_queue_.empty();
}
public:
// 在类中要让线程执行类内成员方法,是不可行的,原因:隐含的参数this
// 所以必须让线程执行静态方法
static void *Routine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tp = (ThreadPool<T> *)args;
while (true)
{
tp->Lock();
while (tp->IsEmpty())
{
// 任务队列为空
tp->Wait();
}
// 该任务队列中一定有任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t.Run();
}
}
void InitThreadPool()
{
pthread_t tid;
for (int i = 0; i < num_; ++i)
{
pthread_create(&tid, nullptr, Routine, (void *)this);
}
}
void PushTask(const T &in)
{
Lock();
task_queue_.push(in);
Unlock();
Wakeup();
}
void PopTask(T *out)
{
*out = task_queue_.front();
task_queue_.pop();
}
~ThreadPool()
{
pthread_mutex_destroy(&mtx_);
pthread_cond_destroy(&cond_);
}
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
} // namespace ns_threadpool
- tcp_server.cc:
// tcp_server.cc
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <pthread.h>
#include "Task.hpp"
#include "thread_pool.hpp"
using namespace ns_threadpool;
using namespace ns_task;
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1.创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(listen_sock < 0){
std::cerr << "socket error: " << errno << std::endl;
return 2;
}
//2. bind
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 3;
}
//3. 设置套接字是listen状态,本质是允许客户端连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0){
std::cerr << "listen error: " << errno << std::endl;
return 4;
}
for( ; ; ){
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// accept
int new_sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)
{
continue;
}
// 打印对端的socket信息
uint16_t cli_port = ntohs(peer.sin_port); // 网络序列转主机序列
std::string cli_ip = inet_ntoa(peer.sin_addr); // 作用与inet_addr函数相反
std::cout << "get a new link -> : [" << cli_ip << ":" << cli_port << "]# " << new_sock << std::endl;
// 线程池版本(单例模式)
//1. 构建一个任务
Task t(new_sock);
//2. 将任务push到后端的线程池即可
ThreadPool<Task>::GetInstance()->PushTask(t);
}
return 0;
}
- tcp_client.cc:
// tcp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " server_ip server_port" << std::endl;
}
// ./tcp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0); // TCP -> SOCK_STREAM
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
// client无需显式地bind,client -> server
// client -> connect!
struct sockaddr_in server;
bzero(&server, sizeof(server)); // 将一段空间清零,不推荐使用
server.sin_family = AF_INET;
server.sin_port = htons(svr_port);
server.sin_addr.s_addr = inet_addr(svr_ip.c_str());
//2. 发起连接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cerr << "connect server failed!" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 进行正常的业务请求
while(true)
{
std::cout << "Please Enter# ";
char buffer[1024];
fgets(buffer, sizeof(buffer)-1, stdin);
write(sock, buffer, strlen(buffer));
ssize_t s = read(sock, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
return 0;
}
运行测试:
5. TCP 简单总结
- 创建 socket 的过程,
socket()
,本质是打开文件 – 仅仅有系统相关的内容。 bind()
,struct sockaddr_in -> IP, port,本质是 IP + port 和文件信息进行关联。listen()
,本质是设置该 socket 文件状态为监听状态,允许客户端来连接。connect()
,本质是发起连接,系统层面:就是构建一个请求报文发送过去。网络层面:发起 TCP 连接的三次握手。accept()
,获取新连接到应用层,是以 fd 为代表的。- 读/写,本质就是进行网络通信,但是对于用户来讲,相当于在进行正常的文件读写。
close()
,关闭文件,系统层面:释放曾经申请的文件资源、连接资源等。网络层面:通知对方,连接已经关闭了,其实就是在进行四次挥手。
当有很多连接连上服务器时,OS 中会存在大量的连接,于是 OS 要管理这些已经建立好的连接。如何管理呢?先描述再组织。
所谓的连接,在 OS 层面上,其实就是一个描述连接的文件结构体。