内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分, 网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
应用程序可以像读写文件一样用read/write在网络上收发数据;
对于IPv4, family参数指定为AF_INET;
对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
protocol参数指定为0即可
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度
如何理解listen第二个参数backlog?
backlog + 1表示在TCP层能够建立正常连接的个数(处于ESTABLISHED状态个数),也就是最多允许有backlog + 1个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略将其置为SYN_RECV状态表明三次握手没有成功, backlog不会设置太大(一般是5)。
注:正常连接的个数并不是只能在服务器维护这几个连接,而是在服务器忙没有来得及accpet取走所维护的连接个数。
其实上面之所以会有处于ESTABLISHED和SYN_RECV状态是因为Linux内核协议栈为一个tcp连接管理使用两个队列:
全连接队列可以没有吗?为什么要维护这个队列?为什么不能太长?
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6, 然而, 各种网络协议的地址格式并不相同。
sockaddr结构
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
sockaddr_in结构
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)];
};
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
/接收套接字上的数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
/返回值:接收到的字节数,出错返回-1
/将消息发给套接字
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数:sockfd:文件描述符
buf:接收缓存区
len:缓存区大小
flags:一般设为0
dest_addr和addrlen为输出型参数
FILE *popen(const char *command, const char *type);
/执行command中的命令,执行结果以文件返回。底层是fock创建子进程利用pipe通信。
下面上手写代码,先说下最终实现功能,通过套接字来进行UDP通信,在客户端上输入简单命令,服务端负责执行然后将执行结果回显到客户端。
server端
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout << "Usage: \n\t" << proc << " server_post" << std:: endl;
}
// ./udp_server serverport
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1.创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr<< "socket create error: " << errno << std::endl;
return 1;
}
uint16_t port = atoi(argv[1]);
// 2.给服务器绑定端口和ip(特殊处理)
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port); //此处的端口号,是我们计算机上的变量,是主机序列
//INADDR_ANY-> 要所有发送到该主机,发送到该端口的数据
local.sin_addr.s_addr = INADDR_ANY; //INADDR_ANY 实际就是值为0的一个宏
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error: " << errno << std::endl;
return 2;
}
//3.提供服务
bool quit = false;
char buffer[1024];
while(!quit)
{
struct sockaddr_in peer; //谁发送过来的
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; //向字符串一样看待
//将buffer当成字符串命令来执行
FILE* fp = popen(buffer, "r"); //popen,创建子进程,执行buffer中的命令,执行结果以文件方式返回
std::string echo_hello;
char line[1024] = {0};
while(fgets(line, sizeof(line),fp) != NULL)
{
echo_hello += line;
}
pclose(fp);
std::cout << "clinet# " << buffer << std::endl;
//发送数据
sendto(sock, echo_hello.c_str(), echo_hello.size(),0, (struct sockaddr*)&peer, len);
}
}
return 0;
}
client端
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout << "Usage: \t" << proc << " server_ip server_post" << std:: endl;
}
// ./client server_ip server_port -->我们在执行的时候把ip和端口号传进来,参数不对就报错
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 0;
}
//1.创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "socket error: " << errno << std::endl;
return 1;
}
//客户端需要显示的bind的吗?
// a.首先,客户端也必须要有ip和port
// b.但是,客户端不需要显示的bind!一旦显示bind就必须明确,client要和哪一个port关联
// client指明的端口号,在client端一定会有吗??有可能被占用,被占用导致client无法使用
// server要的是port必须明确,而且不变,但client只要有就行!一般是由OS自动给你bind()
// 就是client正常发送数据的时候,OS会自动给你bind,采用的是随机端口的方式!
//2.发文件给谁
struct sockaddr_in server;
server.sin_family = AF_INET; //通讯协议
server.sin_port = ntohs(atoi(argv[2])); //端口号
server.sin_addr.s_addr = inet_addr(argv[1]); // ip
//3.使用服务
while(1)
{
//发送数据 ->数据从哪里来
std::cout << "MyShell $ ";
char message[1024] = {0};
fgets(message, sizeof(message), stdin);
//发送数据
sendto(sock, message, sizeof(message), 0, (struct sockaddr*)&server, sizeof(server));
//接收数据
struct sockaddr_in temp; //此处temp就是一个“占位符”
socklen_t len = sizeof(temp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)& temp, &len);
if(cnt > 0)
{
//在网络通信中,只有报文大小,或者字节流中字节个数,没有C/C++中字符串的概念
buffer[cnt] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
{
//出错处理
}
}
return 0;
}
通过套接字来进行TCP通信,在客户端上输入字符串,服务端显示并回显到客户端。
server端
#include "Task.hpp"
#include "thread_pool.hpp"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace ns_task;
using namespace ns_threadpool;
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)
{
std::cout << "client quit ..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
void Usage(std::string proc)
{
std::cout << "Usage: " << proc << "port" << std::endl;
}
// ./tcp_server server_port
int main(int argc, char *args[])
{
if (argc != 2)
{
Usage(args[0]);
return 1;
}
// 1.创建套接字,打开网络文件
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socket creare error: " << errno << std::endl;
return 2;
}
// 2.bind
uint16_t port = atoi(args[1]); //端口号
//建立链接关系
struct sockaddr_in local; //-->
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
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;
}
// 设置套接字是Listen状态, 本质是允许用户连接
const int backlog = 5;
if (listen(listen_sock, backlog) < 0)
{
std::cerr << "listen error " << errno << std::endl;
return 4;
}
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock, (struct sockaddr *)&peer, &len);
if (new_sock < 0)
{
continue;
}
uint16_t cli_port = ntohs(peer.sin_port);
std::string cli_ip = inet_ntoa(peer.sin_addr);
//inet_ntoa函数作用将以网络字节序给出的网络主机地址 in 转换成以点分十进制表示的字符串(如127.0.0.1)。
//结果作为函数返回结果返回。
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,是否会被子进程继承呢? 无论父子进程中的哪一个,强烈建议关闭掉不需要的fd
// child
close(listen_sock);
if (fork() > 0) //不用信号来处理进程资源释放,用爷孙关系
exit(0); //退出的是子进程
//向后走的进程,其实是孙子进程,不用管会被系统领养
ServiceIO(new_sock);
close(new_sock);
exit(0);
}
else
{
// father,不需要等待
waitpid(id, nullptr, 0); //这里等待的时候会不会被阻塞呢? 不会(子进程刚创建就退出了)
close(new_sock);
}
}
return 0;
}
client端
#include
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout << "Usage: " << 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;
}
std::string svr_ip = argv[1];
uint16_t svr_port = (uint16_t)atoi(argv[2]);
//1. 创建socket
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "socket error!" << std::endl;
return 2;
}
//2. bind, 3. listen 4. accept ??
//client无需显示的bind, client->server
//client -> connect!
//2.配置信息
struct sockaddr_in server;
bzero(&server, sizeof(server)); // bzero 和memset作用一样
server.sin_family = AF_INET;
//该函数做两件事情
//1. 将点分十进制的字符串风格的IP,转化成为4字节IP
//2. 将4字节由主机序列转化成为网络序列
server.sin_addr.s_addr = inet_addr(svr_ip.c_str()); //server ip
server.sin_port = htons(svr_port); // server port
//3. 发起链接
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0){
std::cout << "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;
}