认识IP地址和端口号
在IP数据报头,有两个IP地址,分别叫做源IP,和目的IP。
通信的本质:把数据从A主机发送到B主机,并不是目的,而是这两台主机上的应用层上的软件进行通信。通信的两个软件得有特定的标识符来标识其唯一性,有IP(公网)标识一台唯一的主机,对于各自主机上上客户或者服务进程的唯一性由谁来标识呢?肯定不是PID,PID在每次重启服务之后会发生变化,而对于服务器上的进程而言,需要其能够被稳定的访问,所以采用端口号port标识服务器进程,客户端进程的唯一性。
端口号是传输层协议的内容:端口号是一个(uint_16)2字节16位的整数;端口号用来标识一个进程,告诉操作系统, 当前的这个数据要交给哪一个进程来处理;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;一个端口号只能被一个进程占用,但是一个进程可以绑定多个端口号。
所以ip地址(主机全网唯一性)+该主机上的端口号,标识该服务器上进程的唯一性=ipA+portA
,ipB+portb通信。IP地址保证了全网唯一型,port保证了在主机内部的唯一性,那进程都有pid了,为什么要有port呢?a.pid是进程的标识符,而port是网络通信的标识符,需要各自单独设置,系统与网络解耦。b.需要客户端每次都能够找到服务器进程,服务器的唯一性不能做任何改变,不能使用随意能够被更改的值。c.不是所有进程都需要提供网络服务或者请求,但是所有的进程都需要pid。所以进程+port=网络服务进程,底层OS如何根据port找到指定的进程:通过端口号然后在进程中找到对应的PCB(task_struct),其中这个PCB不是简单的一个链路结构,底层是一个hash结构,相互查找的一个比较复杂的关系。由ipA+portA对应了主机上的服务进程,也是全网中唯一的一个进程,ipB+portB对应的服务进程,在全网中是唯一的一个进程,所以网络通信的本质类似于进程间通信,需要让不同的进程先看到同一份资源:网络,并且通信的过程就是在IO的过程,所有的上网行为就两种情况:1、把数据发送出去;2、接受其他进程给我发的数据。
所以在网络通信的过程中,IP+port标识唯一性,client--->server,除了数据,要把自己的ip和port一并发送给对方,到时候需要回传数据能用上。所以这一部分多出来的数据就以协议的形式展现。
协议
TCP(Transmission Control Protocol 传输控制协议):1.传输层协议;2.有连接;3.可靠传输;4.面向字节流
UDP(User Datagram Protocol 用户数据报协议):1.传输层协议;2.无连接;3.不可靠传输;4.面向数据报。
对于TCP和UDP协议而言,TCP可靠是有成本的,往往是比较复杂的,需要维护和编码,而UDP的不可靠,并不是丢包之类的不可靠,只是相对而言比较简单,需要维护和使用。对于不同的场景使用不同的协议,例如银行系统,搜索系统则是需要TCP协议,直播,DNS等UDP协议即可。
网络字节序:对于内存中的多字节数据相对于内存地址有大端和小段之分,网络数据流的数据是大端传输的(小端存储:低地址在低处,高地址在高处;大端存储:低地址在高位,高地址在低位)。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。 TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
//将0x1234abcd写入到0x0000开始的内存中,结果为
big-endian little-endian
0x0000 0x12 0xcd
0x0001 0x34 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12
用以下库函数做网络 字节序和主机字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
其中 h表示host,n表示network,l 和 s分别表示long和short类型。
SOCK预备
首先认识相关函数:
//这里会返回一个文件描述,这个文件描述用作接下来的bind操作
int socket(int domain,int type,int protocol);
//bind函数,通过bind函数可以将用户栈上的数据与OS内核(底层网络)的相关联
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关于UDP中struct sockaddr* addr的结构问题
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同。为了一个接口多用,采用了中间设置不同结构体的形式,然后再强转成struct sockaddr的格式,这样可以保证程序的通用性。
再UDP通信中,我们一般采用sockaddr_in的格式,主要是地址类型,端口号以及IP地址。
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_); //设置为AF_INET即可
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. IP */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
接下来是简单的服务器程序
// udpServer.hpp
//const static 在类内可以定义成整数的形式
//
#include <iostream>
#include <sys/socket.h>
#include <stdio.h>
#include <cstring>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <functional>
namespace Server
{
using namespace std;
enum
{
USAGE_ERROR = 1,
SOCK_ERROR,
BIND_ERROR,
OPEN_ERROR,
POPEN_ERROR
};
// typedef function<void(string,string,string)> func_t;
static const string defaultIP = "0.0.0.0";
using func_t = function<void(int,string, uint16_t, string)>;
class udpserver
{
const static int nummessage = 1024;
public:
udpserver(func_t func, const uint16_t& port, const string &ip = defaultIP)
: _callback(func), _port(port), _ip(ip), _sockfd(-1)
{
}
void initServer()
{
// 1.创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
// Socket报错
cerr << "socket error :" << errno << " : " << strerror(errno) << endl;
exit(SOCK_ERROR);
}
cout<<"socket seccess :"<<_sockfd<<endl;
// 2.绑定端口号
// 服务器要明确的端口号,不能随意改变
struct sockaddr_in local; // 在用户栈上面首先进行操作
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str()); //完成两个操作 string--->uint32_t 大小端转换htonl() ->inet_addr
//local.sin_addr.s_addr = inet_addr(INADDR_ANY); // 一般不明确的绑定某个独立的ip,让上层服务器可以接受来自任意ip上传的信息
// 一般都是绑定任意地址,服务器的真实写法
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n == -1)
{
cerr << "bind error :" << errno << " : " << strerror(errno) << endl;
exit(BIND_ERROR);
}
cout<<"bind success"<<endl;
// UDP的预备工作完成
}
void startServer()
{
// 服务器的本质其实是一个死循环
char buffer[nummessage];
for (;;)
{
// 读取数据
struct sockaddr_in destnation; // 这是一个输入输出型参数,用来接受对方的ip和port
socklen_t len = sizeof(destnation);
ssize_t num = recvfrom(_sockfd, buffer, nummessage, 0, (struct sockaddr *)&destnation, &len);
if (num > 0)
{
buffer[num] = 0; // 将最后一个置为/0
// 获取数据,并且获取是谁发的数据
string clientip = inet_ntoa(destnation.sin_addr);
uint16_t clientport = ntohs(destnation.sin_port);
string message = buffer;
cout << clientip << "[" << clientport << "]" << message << endl;
// 对读取到的数据进行处理
_callback(_sockfd, clientip, clientport, message);
}
}
}
~udpserver()
{
}
private:
uint16_t _port; // 端口号
std::string _ip; // ip地址,实际上一款网络服务器,不建议指明一个IP
int _sockfd;
func_t _callback; // 业务函数
};
}
对于local.sin_addr.s_addr = inet_addr(_ip.c_str());中得inet_addr()主要是完成两个操作。
1、string类型的ip地址,转化成uint32_t类型的,对于平常看到的127.0.0.1(本地环回ip 用于服务器代码的测试,没有经过最底层的物理层,再数据链路层进行返回)这种点分十进制的格式是为了用户便于观看,所出现的形式,而真正再网络中使用的ip地址其实是一个整数类型(uint32_t),对于string类型转化为uint32_t类型的采用库中提供的函数即可,当然也可以自己转化。
uint32_t ip=12345;
struct _ip
{
unsigned char p1;
unsigned char p2;
unsigned char p3;
unsigned char p4;
}
//这里利用结构体的对齐,就可以直接进行强制类型转化
to_string(((struct _ip)&ip)->p1);
to_string(((struct _ip)&ip)->p2);
to_string(((struct _ip)&ip)->p3);
to_string(((struct _ip)&ip)->p4);
2、htonl():完成本地往网络的转化成大端效果。
对于服务器的bind ip而言,一般不指定绑定的ip地址,一般选择的是绑定的是0.0.0.0,便于底下的所有ip都可以向应用层发送消息。
#include "udpServer.hpp"
#include "User.hpp"
using namespace Server;
#include <memory>
#include <unordered_map>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <fstream>
#include <signal.h>
// const string filepath = "./myfile.txt";
// unordered_map<string, string> dick;
static void Uasge()
{
cout << "Usage\n\t"
<< "local_port error" << endl;
}
// static bool Cutstring(string &line, string *key, string *value, const string sep)
// {
// auto pos = line.find(sep);
// if (pos == string::npos)
// return false;
// *key = line.substr(0, pos); //[])
// *value = line.substr(pos + sep.size()); //(]
// return true;
// }
// static void InitDict()
// {
// ifstream in(filepath, std::ios::binary);
// if (!in.is_open())
// {
// cerr << "open file error" << errno << " : " << strerror(errno) << endl;
// exit(OPEN_ERROR);
// }
// string line;
// string key, value;
// while (getline(in, line))
// {
// if (Cutstring(line, &key, &value, ":"))
// dick.insert(make_pair(key, value));
// }
// in.close();
// cout << "load dict success" << endl;
// }
// static void debugPrint()
// {
// for (auto &dt : dick)
// {
// cout << dt.first << " : " << dt.second << endl;
// }
// }
// static void reload(int signo)
// {
// (void)signo;
// InitDict();
// }
// static void handlemessage(const int sockfd, const string &clientip, const uint16_t &clientport, const string &message)
// {
// // 在这里对业务逻辑进行处理,在返回回去
// string response_message; // 返回的字符串函数
// auto iter = dick.find(message);
// if (iter == dick.end())
// response_message = "unknown";
// else
// response_message = iter->second;
// // 开始返回
// struct sockaddr_in respose;
// bzero(&respose, sizeof(respose)); // 记得清空
// respose.sin_family = AF_INET;
// respose.sin_port = htons(clientport);
// respose.sin_addr.s_addr = inet_addr(clientip.c_str());
// sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr *)&respose, sizeof(respose));
// }
// // demo2 解析文件命令
// static void execCmd(const int sockfd, const string &clientip, const uint16_t &clientport, const string &cmd)
// {
// // 需要禁止掉几个命令
// if (cmd.find("rm") != string::npos || cmd.find("cd") != string::npos || cmd.find("rmdir") != string::npos)
// {
// cerr << clientip << "+" << clientport << "正在执行非法操作" << cmd << endl;
// return;
// }
// // popen = exec+fork+pipe
// // 1. cmd解析,ls -a -l
// // 2. 如果必要,可能需要fork, exec*
// string response_message;
// FILE *fp = popen(cmd.c_str(), "r");
// if (fp == nullptr)
// {
// cout << "cmd error" << errno << endl;
// }
// char buffer[1024];
// while (fgets(buffer, sizeof(buffer), fp))
// {
// response_message+=buffer;
// }
// pclose(fp); //记得关闭这个文件描述符
// // 开始返回
// struct sockaddr_in respose;
// bzero(&respose, sizeof(respose)); // 记得清空
// respose.sin_family = AF_INET;
// respose.sin_port = htons(clientport);
// respose.sin_addr.s_addr = inet_addr(clientip.c_str());
// sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr *)&respose, sizeof(respose));
// }
// demo3 --udp聊天
Users onlineusers;
static void chartUdp(const int sockfd, const string &clientip, const uint16_t &clientport, const string &message)
{
// 首先判断是否在线上
if (message == "online")
onlineusers.addUser(clientip, clientport);
if (message == "offline")
onlineusers.delUser(clientip, clientport);
if (onlineusers.isOnline(clientip, clientport))
{
// 开始广播消息
onlineusers.broadcastMessage(sockfd,clientip,clientport,message);
}
else
{
// 只给他单独发消息
string response_message = "您还没有上线,输入online即可上线交流";
struct sockaddr_in respose;
bzero(&respose, sizeof(respose)); // 记得清空
respose.sin_family = AF_INET;
respose.sin_port = htons(clientport);
respose.sin_addr.s_addr = inet_addr(clientip.c_str());
sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr *)&respose, sizeof(respose));
}
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Uasge();
exit(USAGE_ERROR);
}
// signal(2,reload);
// InitDict();
// debugPrint();
uint16_t serverport = atoi(argv[1]);
// unique_ptr<udpserver> se_ptr(new udpserver(handlemessage, serverport)); //翻译
// unique_ptr<udpserver> se_ptr(new udpserver(execCmd, serverport)); //解析命令
unique_ptr<udpserver> se_ptr(new udpserver(chartUdp, serverport)); // 线上聊天
se_ptr->initServer();
se_ptr->startServer();
return 0;
}
对于服务器而言,一般都是死循环,一直在运行服务。
客户端代码:
//udpClient.hpp
#pragma once
#include <iostream>
#include <cerrno>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <strings.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
namespace Client
{
using namespace std;
static const std::string defaultIP = "0.0.0.0";
enum
{
USAGE_ERROR = 1,
SOCK_ERROR,
BIND_ERROR
};
class udpclient
{
public:
udpclient(const string &serverip, const uint16_t &serveport)
: _serverip(serverip), _serverport(serveport), _sockfd(-1), _quit(false)
{
}
void initClient()
{
// 1、socket 套接字操作
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error :" << errno << " : " << strerror(errno) << endl;
exit(SOCK_ERROR);
}
cout << "socket success :" << _sockfd << endl;
// 客户端需不需要bind(要想网络通信,肯定需要bind),需不需要明确的bind(不需要)
// 当我们socket之后,os会自动的帮我们随机bind端口号,不需要我们进行操作
}
// 类内创建线程只能是静态方法
static void *recvMessage(void *args)
{
// 在这里接受返回的信息
int sockfd = *(static_cast<int *>(args));
// 分离子线程,让主线程去干自己的事情,不在等待
pthread_detach(pthread_self());
while (true)
{
char buffer[1024];
struct sockaddr_in answer;
socklen_t temp_len = sizeof(answer);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&answer, &temp_len);
if (n >= 0)
buffer[n] = 0;
// cout<<"服务器的翻译结果是#"<<buffer<<endl;
cout << buffer << endl;
}
return nullptr;
}
void startClient()
{
// 创建子线程
pthread_create(&_readThread, nullptr, recvMessage, (void *)&_sockfd);
// 对se进行初始化
memset(&_se, 0, sizeof(_se));
_se.sin_family = AF_INET;
_se.sin_addr.s_addr = inet_addr(_serverip.c_str());
_se.sin_port = htons(_serverport);
string message;
char cmdLine[1024];
while (!_quit)
{
// 不能使用cout 在遇到空格的时候容易出错
// cout << "please enter #";
// cin>>message;
fprintf(stderr, "Enter# ");
fflush(stderr);
fgets(cmdLine, sizeof(cmdLine), stdin);
cmdLine[strlen(cmdLine) - 1] = 0;
message = cmdLine;
ssize_t num = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_se, (socklen_t)sizeof(_se));
}
}
~udpclient()
{
}
private:
int _sockfd;
uint16_t _serverport; // 目的port
string _serverip; // 目的ip
struct sockaddr_in _se;
bool _quit;
// 线程消息
pthread_t _readThread;
};
}
//udpClient.cpp
#include "udpClient.hpp"
using namespace Client;
#include <memory>
static void Uasge(string proc)
{
cout<<"Usage\n\t"<<proc<<"local_port error"<<endl;
}
//./client ip port
int main(int argc, char* argv[])
{
if(argc!=3)
{
Uasge(argv[0]);
exit(USAGE_ERROR);
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
unique_ptr<udpclient> se_ptr(new udpclient(ip,port));
se_ptr->initClient();
se_ptr->startClient();
return 0;
}
windows端的客户端的代码跟Linux端的代码大同小异
#pragma warning(disable:4996)
#include <iostream>
#include <winsock2.h> //网络socket 套接字必须包含的一个一个头文件
#include <string>
#include <cstring>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
uint16_t port = 8080;
string _ip = "101.43.250.247";
int main()
{
WSADATA wsa; //初始化网络环境
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) //采用的是x.x的网络
{
cout << "WSAStartup() fail " << endl;
exit(-1); //直接中止程序
}
else
{
cout << "WSAStartup() suceess " << endl;
}
//下面跟Linux系统下的操作一摸一样
//开始套接字操作
SOCKET _socket = socket(AF_INET, SOCK_DGRAM, 0); //套接字
if (_socket == SOCKET_ERROR)
{
cout << "socek error" << endl;
exit(-2);
}
struct sockaddr_in in;
memset(&in, 0, sizeof(in));
in.sin_family = AF_INET;
in.sin_port = htons(port);
in.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());
//同样无需进行绑定
char buffer[1024];
string message;
while (true)
{
cout << "Enter##";
fgets(buffer, sizeof(buffer), stdin);
message = buffer;
int sendf = sendto(_socket, message.c_str(), message.size()-1, 0, (struct sockaddr*)&in, sizeof(in));
if (send < 0)
{
cout << "send error" << endl;
exit(0);
}
char buff[1024];
buff[0] = 0; //C风格的清空字符串
struct sockaddr_in from;
memset(&from, 0, sizeof(from));
int len = sizeof(from);
int recvf = recvfrom(_socket, buff, sizeof(buff) - 1, 0,(struct sockaddr*)&from,&len);
if (recvf > 0)
{
buff[recvf] = 0;
cout << "server 返回的消息是" << buff << endl;
}
else break;
}
WSACleanup();
return 0;
}
对于windows端的网络通信,需要包含<windsock2.h>的头文件,同时需要引入 #pragma comment(lib,"ws2_32.lib")。