今天是开学上线下课的第一天,博主没有成功逃课,所以写博客的时候都块晚上了。接下来,博主会连续更新博客,主要因为我的云服务器快过期了~
目录
认识UDP协议
网络字节序
调用接口
IP地址搭配调用接口
inet_addr
inet_ntoa
端口号搭配调用接口(常用)
htons
ntohs
socket常见API
socket(TCP/UDP)
bind(TCP/UDP)
listen(TCP)
accept(TCP)
connect(TCP,客户端)
UDP读取数据调用接口
recvfrom
sendto
注意
执行命令的函数调用
popen
pclose
UDP通信代码实现
udp_client.cc
udp_server.cc
Makefile
运行测试
UDP实现总结
服务端
客户端
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识;
1、传输层协议2、无连接
3、不可靠传输
4、面向数据报
关于UDP更多的知识,会在传输层报文那里详细讲解,现在还不到时候~
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
inet_addr:将点分十进制,人识别的字符串IP地址,转换为4字节整数的IP,并考虑大小端。
inet_ntoa:将4字节整数的IP地址,转换为字符串风格的IP地址。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
举个栗子:
htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
int domain:协议家族类型。
int type:通信类型。
int protocol:选项,前两个参数指明后,最后一个参数不用指明,赋值为0就可以了。
返回值:返回一个新创建的文件描述符。
是不是还算是不太清楚?别急,往下看:
返回值:0表示成功,-1表示失败。
sockfd:socket创建好的文件描述符。
const struct sockaddr* addr:输入型参数,下面详讲。
socklen_t addrlen:定义结构体的大小。
关于const struct sockaddr* addr的详讲:
我们先来看协议家族类型的分类:
我们通常使用AF_INET,记住,绑定的时候有一个细节,参数类型是struct sockaddr*,所以我们要手动强转一下类型。
int sockfd:socket的返回值。
int backlog:能够建立全连接的最大个数+1。(这个到后面再来解释,现在还不太合适,只要记住我们在使用的时候给个5,目前就够了)
返回值:0表示成功,-1表示失败。
int sockfd:socket的返回值。
注意:后面两个参数是输出型参数!可以取出客户端的相关信息。
int sockfd:socket的返回值。
后面两个参数参考上面的bind函数。
int sockfd:socket的返回值。
void* buf:自己提供的缓冲区,要把数据读取到该缓冲区里面。
size_t len:缓冲区大小。
int flags:我们不用关心这个选项,设置为0就可以了。
struct sockaddr* src_addr:输入输出型参数,可以拿到对方的信息,为方便sendto使用。
socklen_t addrlen:结构体大小。
返回值:读取数据字节大小。
int sockfd:socket的返回值。
const void* buf:要发送数据对应缓冲区的地址。
size_t len:缓冲区的大小。
int flags:我们不用关心这个选项,设置为0就可以了。
struct sockaddr* src_addr:输入型参数,要指明通信对端的信息。
socklen_t addrlen:结构体大小。
TCP通信的话,我们通常使用read和write进行读写,这个和UDP还是有一点区别。
const char* command:字符串指令。
const char* type:r->连接到command的标准输出 w->连接到标准输入。
关闭文件指针,防止内存泄漏。
举个栗子:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(string proc) //不能使用引用 --- 这里构造函数
{
cout << "Usage: \n\t" << proc << " server_ip server_port" << endl;
}
//./udp_client server_ip server_port
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)
{
cerr << "socket error: " << errno << endl;
return 1;
}
//客户端需要显示的bind吗??
//a、首先,客户端必须也要有ip和port
//b、但是客户端不需要显示的bind!一旦显示bind,就必须明确,client要和哪一个port关联
//client指明的端口号,在client端一定会有吗?有可能被占用,被占用导致client无法使用
//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]); //字符串IP转化为4字节整型IP,并考虑大小端
//2、使用服务
while(1)
{
//a、你的数据从哪里来?
//string message;
//cout << "输入# ";
//cin >> message;
//旧版本
// string message;
// cout << "输入# ";
// fflush(stdout);
// cin >> message;
//新版本
cout << "MyShell$ ";
char line[1024];
fgets(line, sizeof(line), stdin);
sendto(sock, line, strlen(line), 0, (struct sockaddr*)&server, sizeof(server));
//sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server,sizeof(server));
//此处tmp就是一个"占位符"
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buff[1024];
ssize_t cnt = recvfrom(sock, buff, sizeof(buff)-1, 0, (struct sockaddr*)&tmp, &len);
if(cnt > 0)
{
//在网络通信中,只有报文大小,或者是字节流字节的个数,没有C/C++字符串
//这样的概念(虽然我们后序可能会经常遇到这样的情况)
buff[cnt] = 0;
cout << buff << endl;
}
//cout << "server echo# " << buff << endl;
}
return 0;
}
#include
#include
#include
#include
#include
#include
#include
using namespace std;
const uint16_t port = 8080;
// udp_server细节后面慢慢完善
string Usage(string proc)
{
cout << "Usage: " << proc << " port" << endl;
}
// ./udp_server.cc port
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return -1;
}
uint16_t port = atoi(argv[1]);
// 1、创建套接字,打开网络文件
int sock = socket(AF_INET /*协议种族*/, SOCK_DGRAM /*通信类型*/, 0);
if (sock < 0)
{
cerr << "socket create error: " << errno << endl;
return 1;
}
// 2、给该服务器绑定端口和ip(特殊处理)
struct sockaddr_in local; // _in 网络通信 _un域间通信
local.sin_family = AF_INET; // 16位地址类型
//此处的端口号,是我们计算机上的变量,是主机序列
// a、需要将人识别的点分十进制,字符串风格的IP地址,转换为4字节整数IP
// b、也要考虑大小端
// in_addr_t inet_addr(const char* cp);能完成上面ab两个工作
//坑:
//云服务器,不允许用户直接bind公网IP,另外,实际正常编写的时候,我们也不会指明IP
// local.sin_addr.s_addr = inet_addr("120.53.247.65") [0-255].[0-255].[0-255].[0-255]
// INADDR_ANY: 如果你bind的是确定的IP(主机),意味着只有发到该IP主机上面的数据
//才会交给你的网络进程,但是,一般服务器可能有多张网卡,配置多个IP,我们需要的不是
//某个IP上面的数据,我们需要的是,所有发送到该主机,发送到该端口的数据!
local.sin_port = htons(port); // h to n 主机转网络 s 16位短整型
local.sin_addr.s_addr = INADDR_ANY; //((in_addr_t) 0x00000000) 127.0.0.1本地环回
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind error: " << errno << endl;
return 2;
}
// 3、提供服务
bool quit = false;
#define NUM 1024
char buff[NUM];
while (!quit)
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
//注意: 我们默认认为通信的数据是双方互发字符串
//sizeof(buff)-1 读满时,是可以防止后面buff[cnt] = 0越界
ssize_t cnt = recvfrom(sock, buff, sizeof(buff)-1, 0, (struct sockaddr *)&perr, &len);
if(cnt > 0)
{
buff[cnt] = 0;//0 == '\0', 可以当作一个字符串命令
//r读取buff的字符串内容,识别命令,然后执行
FILE* fp = popen(buff, "r");//r->连接到buff的标准输出 w->连接到标准输入
string echo_hello;
char line[1024] = {0};
while(fgets(line, sizeof(line), fp) != nullptr) //读取失败就返回空,按行读取
{
echo_hello += line;
}
pclose(fp);
cout << "client# " << buff << endl;
//根据用户输入,构建一个新的返回字符串
//echo_hello += "..."
sendto(sock, echo_hello.c_str(), echo_hello.size(), 0, (struct sockaddr*)&perr, len);
}
}
return 0;
}
.PHONY:all
all:udp_client udp_server
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
udp_server:udp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server
1、创建套接字
int sock = socket(AF_INET /*协议种族*/, SOCK_DGRAM /*通信类型*/, 0);
2、绑定ip和端口号
bind(sock, (struct sockaddr *)&local, sizeof(local))
3、提供服务
......
1、创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0)
2、使用服务
......
注意:
客户端不需要自己绑定确定的端口号,因为会存在自己绑定的端口号出现占用的问题。而一个端口只能被一个进程绑定,那么就会出现错误,客户端找客户端端口时出现问题。所以我们不需要手动去绑定,直接交给OSbind就可以了。相反,服务端必须要绑定,因为服务端的ip和端口号一定要被确定,才能被客户访问,服务端通常也不会随意修改端口号。
看到这里,给博主点个赞吧~