查询网络服务的命令
netstat -nlup
n: 显示数字
a:显示所有
u:udp服务
p:显示pid
Recv-Q收到的数量,本地ip和远端ip,00表示可以收到任何地址
网络聊天
服务端
定义一个server类,成员保存ip地址,端口号,文件描述符,是否运行
ip和端口号构造时设置默认值,“0.0.0.0”和“8000”
-
init函数内初始化套接字,返回文件描述符,然后绑定到os。
socket的三个参数,协议族IPv4,套接字类型面向字节流,协议0自动选择,面向字节流默认匹配udp
返回值描述符,是后续功能传入的第一个参数 -
bind函数建立本地捆绑
第二个参数需要sockaddr类型,这个通用类型需要用sockaddr_in类型强转
设置sockaddr_in的结构内容:
初始化为0
设置协议族
ip地址,需要将点分十进制转换为32位整数类型uint32_t,然后转为网络字节序
端口转为网络字节序
ip地址转化方法
先定义ip结构体,每个成员8位,总共4字节
字符串转整数:
1.先去掉.符号,切割为四个整数
2.然后定义一个uint32变量,强转为ip结构体
3.每个部分对应分割的字符串,将字符串转整数
整数转字符串:
1.强制转换为ip结构
2.将每部分转为字符串加上.拼接为点分
- run函数内接收和发送消息
rev第一个参数标识符,第二个接收存放的缓冲区,缓冲区大小,调用方式,0表示阻塞直到有数据或错误,对方的协议结构,协议结构的长度(socklen_t类型)
send参数,标识符,发送内容,内容的大小,调用方式0,不使用特殊行为,对方的协议结构,协议的长度
启动服务端查看有没有服务
ip和port说明
ip可以固定设置,如果是本机ip,虚拟机是可以的,云服务器禁止绑定公网ip。ip填0的意思是,凡是发给我主机的数据,都根据端口号向上交付,如果有两张网卡,固定设置,只会收到发往一个的,0可以收到任一个网卡的,可以提前设置ip,也可以在绑定时设置
addrin.sin_addr.s_addr = INADDR_ANY;
【0,1023】是系统内定的端口号,有固定的应用层协议使用,http:80 https:443 mysql:3306。。也有几个这种特殊的,所以设置端口号时尽量往大一点设置
全
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
enum
{
SOCK_ERR = 1
};
uint16_t defaultport = 8000;
std::string defaultip = "0.0.0.0";
Log log;
class server
{
public:
server(const std::string ip = defaultip, const uint16_t port = defaultport)
{
_ip = ip;
_port = port;
_sockfd = 0;
}
void init()
{
//1.创建套接字
int _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
log.logmessage(fatal, "socket create error:%d", _sockfd);
exit(SOCK_ERR);
}
log.logmessage(info, "socket create success:%d", _sockfd);
//绑定
sockaddr_in addrin;
bzero(&addrin, sizeof(addrin));
addrin.sin_family = AF_INET;
addrin.sin_addr.s_addr = htonl(inet_addr(_ip.c_str()));
addrin.sin_port = htons(_port);
addrin.sin_addr.s_addr = INADDR_ANY;
int ret = bind(_sockfd, (const sockaddr*)&addrin, sizeof(addrin));
if (ret < 0)
{
log.logmessage(fatal, "bind error:%s", strerror(errno));
}
log.logmessage(info, "bind success:%d", ret);
}
void run()
{
_isrunning = true;
char buff[1024];
while (_isrunning)
{
sockaddr client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, &client, &len);
if (n < 0)
{
//log.logmessage(warning, "recv error:%s", strerror(errno));
}
buff[1024] = 0;
std::string echo_string = buff;
echo_string = "client#" + echo_string;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, &client, len);
}
}
~server()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd; // 网路文件描述符
uint16_t _port; // 任意地址bind 0
std::string _ip; // 表明服务器进程的端口号
bool _isrunning;
};
客户端
先初始化端口和ip,这个安装软件使用的时候已经默认设好了。这里定为服务器的ip和刚刚启动服务的端口
设置目标协议addr结构,内容为上面初始化
打开套接字
客户端也是需要绑定的,只不过不是用户显示绑定,由os随机选择,一个端口号只能被一个进程绑定,对服务端也是如此。客户端的port只需要保证主机上的唯一性,在首次发送数据的时候会自动绑定
和服务端一样,发送和接受消息,只不过客户端先发送再接收
全
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
using namespace std;
int main()
{
uint16_t port = 8000;
string ip = "106.54.46.147";
sockaddr_in in;
bzero(&in, sizeof(in));
in.sin_family = AF_INET;
in.sin_addr.s_addr = inet_addr(ip.c_str());
in.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
cout << "socket error" << endl;
}
//client 需要绑定,但不显示绑定,由os自由选择
string message;
char buff[1024];
while (true)
{
cout << "please enter: ";
getline(cin , message);
sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*)&in, sizeof(in));
sockaddr_in rec;
socklen_t len = sizeof(rec);
ssize_t s = recvfrom(sockfd, buff, sizeof(buff) - 1, 0, (sockaddr*)&rec, &len);
if (s > 0)
{
buff[s] = 0;
cout << buff << endl;
}
}
close(sockfd);
}
注意
hton转字节序的函数是根据本机字节序情况转换,如果本机是小端调用几次会转几次
打开云主机端口
如果程序没问题,但收发不到消息,可以试着打开云主机的防火墙端口,以腾讯云为例:
登录控制台,查看详情,防火墙添加规则
服务端自定义数据处理
使用自定义函数类型,run调用实例出的函数,实现数据处理方法的自定义
using func_t = std::function<std::string(const std::string &)>; //定义函数类型
std::string handler(const std::string& str)
{
std::string s = "server get message: ";
s += str;
std::cout << s << std::endl;
return s;
}
命令bash
服务端可以收到信息了,也可以将收到的内容当做其他形式。如果将收到的内容当做xshell命令,就会执行命令
run执行的函数替换。popen函数可以创建子进程通信管道,将传入的命令创建子进程执行。将命令执行的结果添加到字符串内,回显发送给客户端
std::string execute(const std::string& cmd)
{
//safecheck 不想执行的命令安全检查
FILE* fp = popen(cmd.c_str(), "r");
if (fp == nullptr)
{
perror("popen");
return "error";
}
string ret;
char buff[4096];
while (true)
{
char *ok = fgets(buff, sizeof(buff), fp);
if (ok == nullptr)
{
break;
}
ret += buff;
}
pclose(fp);
return ret;
}
本地环回地址
只会在本地协议走一遍,不会发向网络,通常用来测试
xshell
这就是为什么要用xshell登录,就相当于客户端,将命令发到了远端的服务器,接受到字符串执行命令,然后将结果发送回xshell显示
后台里有一个ssh的tcp服务,端口号22号接收发送的命令
win客户端
实现让win客户端和linux服务端网络通讯
win需要包含头文件WinSock2.h头文件和ws2_32.lib网络库
上面的头文件要在windows.h这个头文件之前,不然会报错
首先初始化网络版本信息,addr协议内容
打开套接字
收发功能和前面一样
清理数据
两边汉字编码不一样,可能会显示有问题
全
#include <iostream>
#include <string>
#include <WinSock2.h>
//#include <Windows.h>
//关闭安全警告
#pragma warning(disable:4996)
//包含网络库
#pragma comment(lib, "ws2_32.lib")
using namespace std;
string ip = "106.54.46.147";
uint16_t port = 8000;
int main()
{
//初始化socket数据
WSADATA wsd;
WSAStartup(MAKEWORD(2, 2), &wsd);
sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.S_un.S_addr = inet_addr(ip.c_str());
server.sin_port = htons(port);
//套接字
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == SOCKET_ERROR)
{
cout << "sock error" << endl;
return 1;
}
string message;
char buff[1024];
while (true)
{
cout << "please enter: ";
getline(cin, message);
sendto(sockfd, message.c_str(), message.size(), 0, (const sockaddr*) & server, sizeof(server));
sockaddr_in temp;
int len = sizeof(temp);
int s = recvfrom(sockfd, buff, 1023, 0, (sockaddr*)&temp, &len);
if (s > 0)
{
buff[s] = 0;
cout << buff << endl;
}
}
closesocket(sockfd);
WSACleanup();
return 0;
}
网络聊天室 (多线程)
聊天室功能需要用户名标识每一个主机,这里用对方的ip和port作为区分,发言的前缀
用一个无序map容器,string作为索引,值存addr结构
将收到的addr结构转为主机序列加入到显示屏幕的内容
检查如果是新的主机就加入到_online结构,并显示新主机加入
遍历容器内容,将最新消息发送给所有用户
sendto可以自动转网络序列,所以容器内容不需要转换
上面可以实现接收到不同用户的消息,但有个问题,客户端的getline没有内容时是一直阻塞住的,不输入内容的时候收不到其他消息。所以用多线程,一个线程收消息,一个线程发消息
两个线程都需要文件描述符和addr结构
定义一个结构体,将初始化的内容赋值给结构体
这时可以显示其他人的消息,但自己因为发送和接收的顺序无法正常接收。需要将输入和输出的终端分开
/dev/pts 这个文件里保存了所有打开的终端
0号终端重定向到6,显示到了1号页面中,所以文件6代表了下面这个终端,5是上面的终端。在接收消息后想显示到上面的终端,下面的用来发送,由于线程的文件描述共享,所以将标准错误重定向到5,在下面打开服务端
上面的内容可以简化,打开两个端口,提前确定哪个用来显示,另一个启动客户端,将标准错误重定向到显示端
全
服务端
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"
enum
{
SOCK_ERR = 1
};
uint16_t defaultport = 8000;
std::string defaultip = "0.0.0.0";
using func_t = std::function<std::string(const std::string &)>; //定义函数类型
//typedef std::function<std::string(const std::string &)> func_t
Log log;
class server
{
public:
server(const std::string ip = defaultip, const uint16_t port = defaultport)
{
_ip = ip;
_port = port;
_sockfd = 0;
}
void init()
{
//1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
log.logmessage(fatal, "socket create error:%d", _sockfd);
exit(SOCK_ERR);
}
log.logmessage(info, "socket create success:%d", _sockfd);
//绑定
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
//local.sin_addr.s_addr = INADDR_ANY;
int ret = bind(_sockfd, (const sockaddr*)&local, sizeof(local));
if (ret < 0)
{
log.logmessage(fatal, "bind error:%s", strerror(errno));
}
log.logmessage(info, "bind success:%d", ret);
}
void checkuser(const struct sockaddr_in& sock, const string ip, const uint16_t port)
{
auto it = _online.find(ip);
if (it == _online.end())
{
_online.insert({ip, sock});
cout << "[" << ip << ":" << port << "] add new user" << endl;
}
}
void broadcast(const string& message, string ip, uint16_t port)
{
for (const auto user : _online)
{
socklen_t len = sizeof(user.second);
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
}
}
void run(func_t func)
{
_isrunning = true;
char buff[1024];
while (_isrunning)
{
sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (sockaddr*)&client, &len);
if (n < 0)
{
//log.logmessage(warning, "recv error:%s", strerror(errno));
continue;
}
buff[n] = 0;
//获取端口号和ip
string ip = inet_ntoa(client.sin_addr);
// 网络字节序转主机
uint16_t port = ntohs(client.sin_port);
checkuser(client, ip, port);
std::string echo_string = "[" + ip + ":" + to_string(port) + "]: " + buff;
broadcast(echo_string, ip, port);
// std::cout << echo_string << endl;
// echo_string = func(echo_string);
//sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);
}
}
~server()
{
if (_sockfd > 0)
close(_sockfd);
}
private:
int _sockfd; // 网路文件描述符
uint16_t _port; // 任意地址bind 0
std::string _ip; // 表明服务器进程的端口号
bool _isrunning;
//用ip检索
unordered_map<string, struct sockaddr_in> _online; //注册主机
};
客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <fcntl.h>
using namespace std;
string path = "/dev/pts/5";
struct thread_data
{
int _sockfd;
struct sockaddr_in _server;
string _ip;
};
void *send_message(void * temp)
{
thread_data *td = (struct thread_data*)temp;
string message;
cout << "welcome " << td->_ip << endl;
while (true)
{
cout << "please enter: ";
getline(cin, message);
sendto(td->_sockfd, message.c_str(), message.size(), 0, (const sockaddr *)&td->_server, sizeof(td->_server));
}
}
void* recv_message(void* temp)
{
// int fd = open(path.c_str(), O_WRONLY);
// if (fd < 0)
// {
// perror("open");
// }
// dup2(fd, 2);
thread_data *td = (struct thread_data *)temp;
char buff[1024];
sockaddr_in rec;
socklen_t len = sizeof(rec);
while (true)
{
ssize_t s = recvfrom(td->_sockfd, buff, sizeof(buff) - 1, 0, (sockaddr *)&rec, &len);
if (s > 0)
{
buff[s] = 0;
cerr << buff << endl;
}
}
//close(fd);
}
int main()
{
thread_data td;
uint16_t port = 8000;
//string ip = "106.54.46.147";
string ip = "106.54.46.147";
sockaddr_in in;
bzero(&in, sizeof(in));
td._server.sin_family = AF_INET;
td._server.sin_addr.s_addr = inet_addr(ip.c_str());
td._ip = ip;
td._server.sin_port = htons(port);
td._sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td._sockfd < 0)
{
cout << "socket error" << endl;
}
//client 需要绑定,但不显示绑定,由os自由选择
pthread_t tid_send;
pthread_t tid_recv;
pthread_create(&tid_send, nullptr, send_message, &td);
pthread_create(&tid_recv, nullptr, recv_message, &td);
pthread_join(tid_send, nullptr);
pthread_join(tid_recv, nullptr);
close(td._sockfd);
}
地址转换函数
介绍基于ipv4的socket网络编程,struct in_addr sin_addr表示32位的ip地址,但是我们通常用点分十进制字符串表示ip地址,一下函数可以在字符串表示和in_addr表示之间转换
其中inet_pton和inet_ntop不仅可以转换ipv4的in_addr,还可以转换ipv6的in6_addr,因此接口是void* addrptr
关于inet_ntoa
这个函数返回一个char*,自己内部申请了一块内存保存ip的结果,那么需不需要手动释放呢?
man手册说返回结果放在了静态存储区,不需要手动释放
当多次调用时,两个不同ip都会转换为相同的结果,以最后一个为准
因为inet_ntoa把结果放到内部的静态缓存区,第二次调用的时候会覆盖上一次结果。APUE中,明确提出inet_ntoa不是线程安全的函数,但是centos7上测试,没有出现问题,可能是内部实现加了互斥锁,在多线程环境下,推荐使用inet_ntop,这个函数由用户提供缓冲区结果,可以规避线程安全的问题