目录
一.使用函数详解
1.socket函数
2.bind函数
3.IP地址转化函数
4. recvfrom函数
5.sendto函数
二.测试代码
1.本地环回测试
2.绑定INADDR_ANY的回复服务器
3.简易xshell
头文件: #include
, #include
函数: int socket(int domain, int type, int protocol);参数:
①domain:
- AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
- AF_INET6 与上面类似,不过是来用IPv6的地址
- AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
②type:
- SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接. 这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
- SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
③protocol: 传0 表示使用默认协议。
返回值: 成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
socket接口底层做了什么
创建套接字本质就是打开了一个文件
创建套接字的过程其实就是在底层创建:
①进程调用socket函数前的结构
②调用后的结构
③其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*和write*)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。
头文件: #include
#include 函数: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- ①sockfd:socket文件描述符,就是我们创建套接字时获取到的文件描述符。
- ②addr:网络相关的属性信息,包括协议家族、IP地址、端口号等
- ③addrlen:传入的addr结构体的长度 , sizeof(addr)长度
返回值:成功返回0,失败返回-1, 设置errno
补充:
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
- bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。
(1)struct sockaddr_in 结构体
struct sockaddr_in当中的成员如下:
(2)如何理解绑定?
(3)整数IP vs 字符串IP
(4)字符串IP和整数IP相互转换的方式
由于联合体的空间是成员共享的,因此我们设置IP和读取IP的方式如下:
注意: 在操作系统内部实际用的就是位段和枚举,来完成字符串IP和整数IP之间的相互转换的。
(1)inet_addr函数
(2)inet_ntoa函数
函数: ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
- sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
- buf:读取数据的存放位置。
- len:期望读取数据的字节数。
- flags:读取的方式。一般设置为0,表示阻塞读取。
- src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等(谁发给你的)
- addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
返回值说明:
读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
注意:
- 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
- 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
- 由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。
如何甄别你是应用层的一个攻击程序?
服务器能拿到客户端的ip , 对ip进行统计,如果次数大于某一特定值,可能是攻击程序。
函数: 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:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入dest_addr结构体的长度。
返回值说明:
写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。
注意:
- 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
- 由于sendto函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转
(1)代码
①Makefile ,一次生成两个可执行程序
CC=g++
.PHONY:all
all:server client
server:udp_server.cc
$(CC) -o $@ $^ -std=c++11
client:udp_client.cc
$(CC) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client
②udp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#define DEFAULT 8080
#define SIZE 128
class UdpServer{
private:
std::string ip;
int port;
int sockfd;
public:
UdpServer(std::string _ip,int _port = DEFAULT):ip(_ip),port(_port)
{}
~UdpServer()
{
}
public:
bool InitUdpServer()
{
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << sockfd << std::endl;
struct sockaddr_in local; //创建变量初始化
memset(&local , '\0' ,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port); //主机序列转网络序列16为端口号
local.sin_addr.s_addr = inet_addr(ip.c_str()); //将点分十进制的IP转化为 整数ip
if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
return false;
}
std::cout << "bind success " << std::endl;
return true;
}
void Start()
{
char buffer[SIZE];
for(;;){
struct sockaddr_in peer; //哪一端发送的数据
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(size > 0){
buffer[size] = 0 ;
int _port = ntohs(peer.sin_port);
std::string _ip = inet_ntoa(peer.sin_addr);
std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
}
else{
std::cerr << "recvfrom error!" << std::endl;
}
}
}
};
③udp_server.cc
#include "udp_server.hpp"
int main(int argc , char* argv[])
{
if(argc != 2){
std::cout << "Usage: " << argv[0] << " port " << std::endl;
return 1;
}
std::string ip = "127.0.0.1";
int port = atoi(argv[1]); //字符串转化为整数
UdpServer* svr = new UdpServer(ip,port);
svr->InitUdpServer();
svr->Start();
return 0;
}
④udp_client.hpp
#pragma once
#include
#include
#include
#include
#include
class UdpClient{
private:
std::string ip;
int port;
int sockfd;
public:
UdpClient(std::string _ip,int _port):ip(_ip),port(_port)
{}
~UdpClient()
{}
public:
bool InitUdpClient()
{
sockfd = socket(AF_INET ,SOCK_DGRAM ,0);
if(sockfd < 0){
std::cerr << "socket error!" << std::endl;
return false;
}
//客户端不需要port? 客户端不需要绑定?
return true;
}
void Start()
{
struct sockaddr_in peer; //往哪发
memset(&peer , 0 ,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str()); //点分十进制转整形
std::string msg;
for(;;){
std::cout << "Please Enter# ";
std::cin >> msg;
sendto(sockfd ,msg.c_str(),msg.size(), 0 ,(struct sockaddr*)&peer ,sizeof(peer));
}
}
};
客户端是否需要bind的问题
1.Server端对应的尤其是IP和端口号,是不能轻易被更改的。必须是确定的,众所周知的,而且永远不会改。
2.为什么server端口号不能改?
因为服务器面对的客户是成百上千的,服务器—旦把port改了,那么客户端就找不到了,就没办法访问了, 只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。
3.客户端不需要bind ?
4.为什么系统自动bind的时候不会出错,冲突?
只有OS最清楚port的使用情况(port也是有资源上限的,OS会把port管理起来)
⑤udp_client.cc
#include"udp_client.hpp"
int main(int argc ,char* argv[])
{
if(argc != 3){
std::cout << "Usage: " << argv[0] << " ip port " << std::endl;
return 1;
}
std::string ip = argv[1];
int port = atoi(argv[2]);
UdpClient* ucli = new UdpClient(ip,port);
ucli->InitUdpClient();
ucli->Start();
}
(2)结果
①server端运行,套接字是创建成功的,对应获取到的文件描述符就是3
②使用netstat命令查看网络状态:
常用选项:
属性信息:
③多个client可以和server通信, 客户端也已经动态绑定成功了
INADDR_ANY
①.服务器的公网IP是49.232.80.153,这里使用ping命令,显示网络良好
②.将服务端设置的本地环回改为我的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败。
③.由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。
因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。
绑定INADDR_ANY的优点
1..网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址
2.如果绑定的是确定的IP,意味着只会接受特定绑定IP发送过来的数据,其他发送过来的数据就接受不到了。IP本质是标定主机的,所有发给这台主机上的数据到IP层都应该给我。所以经常使用INADDR_ANY来表示任意绑定。
3.在Linux内核当中,这个INADDR ANY选项本质在内核的逻辑上起了一个判断的作用;如果IP== INADDR ANY收上来的所有IP报文都交给我,如果绑定的是具体的IP,只有从这个IP上上来的报文才会给你。
(1)代码
①udp_server.hpp , 绑定INADDR_ANY , 发送数据到client
#pragma once
#include
#include
#include
#include
#include
#include
#include
#define DEFAULT 8080
#define SIZE 128
class UdpServer{
private:
int port;
int sockfd;
public:
UdpServer(int _port = DEFAULT):port(_port)
{}
~UdpServer()
{
if(sockfd >= 0)
close(sockfd);
}
public:
bool InitUdpServer()
{
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << sockfd << std::endl;
struct sockaddr_in local; //创建变量初始化
memset(&local , '\0' ,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port); //主机序列转网络序列16为端口号
local.sin_addr.s_addr = INADDR_ANY; //接收任意IP发来的消息
if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
return false;
}
std::cout << "bind success " << std::endl;
return true;
}
void Start()
{
char buffer[SIZE];
for(;;){
struct sockaddr_in peer; //哪一端发送的数据
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(size > 0){
buffer[size] = 0 ;
int _port = ntohs(peer.sin_port);
std::string _ip = inet_ntoa(peer.sin_addr);
std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
std::string echo_msg = "server get -> ";
echo_msg += buffer;
sendto(sockfd , echo_msg.c_str(),echo_msg.size(),0,(struct sockaddr*)&peer,len);
}
else{
std::cerr << "recvfrom error!" << std::endl;
}
}
}
};
②udp_client.hpp , 接收数据 ,打印出来
#pragma once
#include
#include
#include
#include
#include
#include
class UdpClient{
private:
std::string ip;
int port;
int sockfd;
public:
UdpClient(std::string _ip,int _port):ip(_ip),port(_port)
{}
~UdpClient()
{
if(sockfd >= 0){
close(sockfd);
}
}
public:
bool InitUdpClient()
{
sockfd = socket(AF_INET ,SOCK_DGRAM ,0);
if(sockfd < 0){
std::cerr << "socket error!" << std::endl;
return false;
}
//客户端不需要port? 客户端不需要绑定?
return true;
}
void Start()
{
struct sockaddr_in peer; //往哪发
memset(&peer , 0 ,sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str()); //点分十进制转整形
std::string msg;// 存储发送给server的消息
for(;;){
std::cout << "Please Enter# ";
getline(std::cin,msg);
sendto(sockfd ,msg.c_str(),msg.size(), 0 ,(struct sockaddr*)&peer ,sizeof(peer));
char buffer[128];
struct sockaddr_in temp;
socklen_t len =sizeof(temp);
ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
if(size > 0){
buffer[size] = 0;
std::string _ip = inet_ntoa(temp.sin_addr);
int _port = ntohs(temp.sin_port);
std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
}
}
}
};
(2)结果
①两个IP都能访问server
②该服务器的本地IP地址变成了0.0.0.0
,这就意味着该UDP服务器可以在本地读取任何一张网卡里面端口号为8081的数据。
(1)代码
只需要修改udp_server.hpp的代码
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEFAULT 8080
#define SIZE 128
class UdpServer{
private:
int port;
int sockfd;
public:
UdpServer(int _port = DEFAULT):port(_port)
{}
~UdpServer()
{
if(sockfd >= 0)
close(sockfd);
}
public:
bool InitUdpServer()
{
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0){
std::cerr << "socket error" << std::endl;
return false;
}
std::cout << "socket create success, sockfd: " << sockfd << std::endl;
struct sockaddr_in local; //创建变量初始化
memset(&local , '\0' ,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port); //主机序列转网络序列16为端口号
local.sin_addr.s_addr = INADDR_ANY; //接收任意IP发来的消息
if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
std::cerr << "bind error" << std::endl;
return false;
}
std::cout << "bind success " << std::endl;
return true;
}
void Start()
{
char buffer[SIZE];
for(;;){
struct sockaddr_in peer; //哪一端发送的数据
socklen_t len = sizeof(peer);
ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(size > 0){
buffer[size] = 0 ;
int _port = ntohs(peer.sin_port);
std::string _ip = inet_ntoa(peer.sin_addr);
std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
int pipes[2];
pipe(pipes);//匿名管道
pid_t id = fork();
if(id == 0){
//child
close(pipes[0]);
dup2(pipes[1], 1);
execlp(buffer, buffer,nullptr);//进程替换
exit(1);
}
//father
std::string result;
close(pipes[1]);
char c;
while(1){
if(read(pipes[0], &c, 1) > 0){
result.push_back(c);
}
else{
break;
}
}
wait(nullptr);
std::string echo_msg;
if(result.empty()){
echo_msg = "server get!->";
echo_msg += buffer;
}
else{
echo_msg = result;
}
sendto(sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);
}
else{
std::cerr << "recvfrom error!" << std::endl;
}
}
}
};
(2)结果 , 只能执行不带选项的简单命令