我们知道IP地址是用来标识主机唯一性的。
而源IP地址表示从哪个主机来, 目的IP地址表示去哪个主机。
端口号(port):
1,标识进程唯一性的。也就是说,一个端口号用来标识一个进程。
2,端口号是一个2字节16位的整数。
同样的:多个端口号可以对应一个进程, 多个进程不能对应一个端口。
因为必须通过端口号找到唯一一个进程。
IP地址 + 端口号(port) 就可以表示互联中的一台主机中的一个进程。
源端口号和目的端口号:传输层的数据段中有两个端口号,分别叫做目的端口号和源端口号,表示数据是发给哪个进程,
由哪个进程发的
我们知道,网络通信的本质就是进程间通信。
TCP协议:
1,传输层协议
2,有链接
3,可靠
4,面向字节流(数据从一方流向另一方)。
UDP协议
1,传输层协议
2,无连接
3,不可靠
4,面向数据报。
这里的可靠和不可靠是中性词,可靠肯定是要更多资源去维护的。如银行取钱那肯定是要可靠的。
其他的不一定要可靠。
网络字节序的概念:
其实在内存中分大小端。
小端:数据的低位存放在低地址
大端:数据的低位存放在高地址
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
网络字节序和主机字节序的相关转换函数:
#include
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网络。
UDP协议下的相关函数操作
服务端:
#include
#include
#include
#include
#include //这两个头文件是 struct sockaddr_in
#include
using namespace std;
void Usage(const char* proc)
{
cout << "Usage:" << endl << "./server " << "server.port";
}
// ./server server.port
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1 创建套接字(网络文件), sock---文件描述符
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
cout << "sock fail" << endl;
return 2;
}
//2 bind 绑定ip地址和端口号port
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY; //服务端可以任意绑定
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cout << "bind fail" << endl;
return 3;
}
//提供服务
char buff[1024];
while(1)
{
struct sockaddr_in peer;
memset(&peer, 0 , sizeof(peer));
socklen_t len = sizeof(peer);
//peer 返回的客户端的ip + 端口号的属性集合 sockaddr
ssize_t s = recvfrom(sock, buff, sizeof(buff) , 0, (struct sockaddr*)&peer, &len);
if(s > 0)
{
//成功接受
buff[s] = 0;
cout << "clinet #" << buff << endl;
const char* ch = "我是服务器, 你好啊";
sendto(sock, ch, strlen(ch), 0, (struct sockaddr*)&peer, len);
}
else
{
cout << "recvfrom fail" << endl;
return 3;
}
}
return 0;
}
客户端:
#include
#include
#include
#include
#include //这两个头文件是 struct sockaddr_in
#include
using namespace std;
void Usage(const char* proc)
{
cout << "Usage" << endl << proc << "server.ip server.port" << endl;
}
// ./client server.ip server.port
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
//1 创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
cout << "sock fail" << endl;
return 1;
}
//不需要绑定,OS会自动绑定
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
while(1)
{
cout << "client:" ;
char line[1024];
cin >> line;
sendto(sock, line, sizeof(line), 0, (struct sockaddr*)&server, sizeof(server));
//ret 占位用,无其他作用
struct sockaddr_in ret;
socklen_t len = sizeof(ret);
char buff[1024];
ssize_t s = recvfrom(sock, buff, sizeof(buff), 0, (struct sockaddr*)&ret, &len);
if(s < 0)
{
cout << "client recvfron fail" << endl;
return 4;
}
cout << "server say" << buff << endl << endl;;
}
return 0;
}
相关操作函数的总结:
1,创建socket的过程,socket()本质是创建网络文件,与系统相关
2,bind() sockaddr_in ----> 填入ip + port 本质是将ip + port 与网络文件相关联
3, listen() 设置该socket文件的状态,允许客户端来链接我
4 ,accept() 获取新连接到应用层,以文件描述符为代表,OS中肯定会存在大量的链接,如何管理呢?—>先描述,再组织
5, read/write 本质就是进行网络通信,而对于用户来说,就是在进行文件的正常读写。
6, close() ,关闭文件, 系统层面上,释放曾经申请的文件资源和链接资源
7, connect() 本质是发起链接, 在系统层面上,构建一个报文发过去。
服务端:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
void Usage(const char* proc)
{
cout << "Usage:" << endl << proc << " port" << endl;
}
void ServerIO(int new_sock)
{
//开始服务
while(1)
{
char buff[1024];
memset(buff, 0, sizeof(buff));
ssize_t s = read(new_sock, buff, sizeof(buff));
if(s > 0)
{
cout << "client say#" << buff << endl;
const char* ch = "server recv";
write(new_sock, ch, strlen(ch));
}
else if(s == 0)
{
//对端关闭
}
else
{
cout << "new_sock fail " << endl;
return ;
}
}
}
void* handler(void* args)
{
int* p = (int*)args;
pthread_detach(pthread_self());
ServerIO(*p);
close(*p);
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
//1, 创建套接字(网络文件) , AF_INFT是ipv4, sock_stream 是面向字节流
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cout << "sock fail" << endl;
return 2;
}
//2 bind
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[1]));
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cout << "bind error" << endl;
return 3;
}
//3 监听
if(listen(sock,5) < 0)
{
cout << "listen fail" << endl;
return 4;
}
//4 提供服务
while(1)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//new sock 是提供服务的套接字, sock是用于监听的套接字
// 提供服务 在线拉客
int new_sock = accept(sock, (struct sockaddr*)&peer, &len);
if(new_sock < 0)
{
continue;
}
//通过创建进程来实现可对多个客户同时来进行服务
/*signal(SIGCHLD, SIG_IGN);
pid_t id = fork();
if(id == 0)
{
//子进程关闭与自己无关的文件描述符
close(sock);
ServerIO(new_sock);
close(new_sock);
}
else if(id < 0)
{
return 7;
}
else
{
//panrent
}
*/
//通过创建线程的方式来实现对多个客户同时进行服务
pthread_t tid;
int* pram = new int(new_sock);
pthread_create(&tid, nullptr, handler, pram);
}
return 0;
}
客户端:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
//./client server.ip server.port
int main(int argc, char* argv[])
{
//1 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cout << "socket error " << endl;
return 1;
}
//不需要显示绑定, 只用主动链接
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]);
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
cout << "connect fail" << endl;
return 2;
}
char buff[1024];
while(1)
{
cout << "请输入您的请求:";
cin >> buff;
write(sock, buff, strlen(buff));
ssize_t s = read(sock, buff, sizeof(buff));
if(s > 0)
{
buff[s] = 0;
cout << buff << endl;
}
else
{
cout << "read error" << endl;
}
}
return 0;
}
这样就可以完成通信了
先整理反思一下:上面的代码用于进行网络通信,本质到底是在做什么呢?
其实,本质上是在编写应用层,从0 ----> 1。
1,我们定义一个结构体来表示我们要交互的数据。
2,发送数据的时候将这个结构体转换成一个字符串,接收端接受后将这个字符串还原成一个结构体。
这样做有什么好处呢? 意义又是什么呢?
1,首先啊, 结构化的数据一定是不方便网络传输的,另外占用空间大,例如还需要内存对齐。所以,将其转换为字符串,
可以节省资源, 可以方便应用层通信的方便。
2,应用层也可以很方便的取出字符串中的成员,不需要关系网络到底是如何传输的。 -----> “解耦”
写代码使用感受下(不需要了解原理, 会用即可)
#include
#include
#include
using namespace std;
typedef struct request
{
int _x;
int _y;
char _op;
}request_t;
int main()
{
request_t req = {10, 20, '+'};
//序列化的过程
Json::Value root; //root可以承载任何对象的, 利用的是K-V模型
root["data1"] = req._x;
root["data2"] = req._y;
root["op"] = req._op;
//Json::FastWriter writer;
Json::StyledWriter writer;
string Json_string = writer.write(root);
cout << Json_string << endl;
//对Json_string 进行反序列化
Json::Reader reader;
Json::Value root1;
reader.parse(Json_string, root1); //将字符串Json_string 序列化到root1里面
request_t req1;
req1._x = root1["data1"].asInt();
req1._y = root1["data2"].asInt();
req1._op = (char)root1["op"].asInt();
cout << req1._x << req1._op << req1._y << endl;
return 0;
}
序列化的过程:
反序列化的过程
Sock.hpp
#include
#include
#include
#include
#include //struct sockaddr_in
#include
using namespace std;
class Sock
{
public:
//创建套接字, 本质是创建网络文件
static int Socket()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
cout << "sock error" << endl;
exit(1);
}
return sock;
}
//服务端需要绑定, 客户端不需要绑定,由OS自动绑定
static void Bind(int sock, uint16_t port)
{
struct sockaddr_in local;
local.sin_family = AF_INET; //ipv4协议
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY; //链接服务器上的任意一台主机
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cout << "bind error" << endl;
exit(2);
}
}
//服务端需要监听----监听状态
static void Listen(int sock)
{
if(listen(sock, 5) < 0)
{
cout << "listen error" << endl;
exit(3);
}
}
//服务端需要接受,新的fd实际上提供服务的文件,旧的sock是用来监听的----> "饭馆的拉客少年"
static int Accept(int sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int fd = accept(sock, (struct sockaddr*)&peer, &len); //peer是输出型参数,返回的是客户端相关属性
if(fd < 0)
{
cout << "accept error" << endl;
exit(4);
}
return fd;
}
//客户端需要链接的是服务端 需要 ip + port
static void Connect(int sock, string ip, uint16_t port)
{
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip.c_str());
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cout << "connect error" << endl;
exit(5);
}
cout << "connect success" << endl;
}
};
protocol.hpp
#include
#include
#include
using namespace std;
typedef struct request
{
int _x;
int _y;
char _op;
}request_t;
typedef struct response
{
int code; // 0是正常, -1是异常
int result;
}response_t;
//将reques序列化 struct request -----> string
string serializeRequest(const request_t& req)
{
//request_t -----> string
Json::Value root; //root可以承装任何对象 K-V模型
root["datax"] = req._x;
root["datay"] = req._y;
root["op"] = req._op;
Json::FastWriter writer;
string ret = writer.write(root);
return ret;
}
//反序列化 : 将string ------> struct request
void deserializeRequest(const string& req_string, request_t& out)
{
Json::Reader reader;
Json::Value root;
reader.parse(req_string, root);
out._x = root["datax"].asInt();
out._y = root["datay"].asInt();
out._op = (char)root["op"].asInt();
}
//序列化 : 将struct reponse -----> string
string serializeResponse(const response_t& res)
{
Json::Value root;
root["datax"] = res.code;
root["datay"] = res.result;
Json::FastWriter writer;
string ret = writer.write(root);
return ret;
}
//反序列化 ,将string ----- > struct response
void deserializeResponse(const string& res_string, response_t& out)
{
Json::Reader reader;
Json::Value root;
reader.parse(res_string, root); //将string 的数据填入 root
out.code = root["datax"].asInt(); //拿出来还是字符串,要转为整形。
out.result = root["datay"].asInt();
}
如何来完成计算机的功能呢?
客户端: 创建struct request -------> req_string ------>fd ; fd --------> res_string------>struct response;
服务端: fd------->req_string------>struct req; 创建struct response -------> res_string ---------->fd
服务端:
#include "Sock.hpp"
#include "protocal.hpp"
void Usage(const char *proc)
{
cout << "Usage:" << endl;
cout << proc << " port" << endl;
}
void *handlerRequest(void *argv)
{
int fd = *(int *)argv;
delete (int *)argv;
pthread_detach(pthread_self());
char buff[1024];
request_t req;
ssize_t s = read(fd, buff, sizeof(buff));
if (s > 0)
{
//读取成功, 读取的是字符串, 我们需要服务端需要做什么呢?
// 1, 读取字符串request, 解决问题--->struct response --->转成字符串传回去。
buff[s] = 0;
cout << "get a new request" << endl;
string str = buff;
deserializeRequest(str, req); //反序列化的过程, 将string ---->struct request
response_t res = {0, 0};
switch (req._op)
{
case '+':
res.result = req._x + req._y;
break;
case '-':
res.result = req._x - req._y;
break;
case '*':
res.result = req._x * req._y;
break;
case '/':
if(req._y == 0)
{
res.code = -1;
}
else
res.result = req._x / req._y;
break;
default:
res.code = -2;
break;
}
cout << "request" << req._x << req._op << req._y << endl;
string ret = serializeResponse(res);
write(fd, ret.c_str(), ret.size()); // 将字符串写回到fd
cout << "服务成功" << endl;
close(fd);
}
}
// ./server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return 1;
}
int listen_sock = Sock::Socket(); // 创建套接字, 本质网络文件
Sock::Bind(listen_sock, atoi(argv[1]));
Sock::Listen(listen_sock);
//开始提供服务
while (1)
{
int fd = Sock::Accept(listen_sock);
if (fd < 0)
{
cout << "accept error" << endl;
return 2;
}
cout << "get a new link" << endl;
int *pram = new int(fd);
pthread_t tid;
pthread_create(&tid, nullptr, handlerRequest, pram);
}
return 0;
}
客户端:
#include "Sock.hpp"
#include "protocal.hpp"
// ./client server.ip server.port;
int main(int argc, char* argv[])
{
//usage
int sock = Sock::Socket(); // 创建网络文件
//不需要bind
Sock::Connect(sock, argv[1], atoi(argv[2]));
request_t req;
cout << "one data:";
cin >> req._x;
cout << "two data:";
cin >> req._y;
cout << "op:";
cin >> req._op;
string ret = serializeRequest(req);
write(sock, ret.c_str(), ret.size());
char buff[1024];
ssize_t s = read(sock, buff, sizeof(buff));
if(s > 0)
{
response_t res;
string ret = buff;
deserializeResponse(ret, res);
cout << "code:" << res.code << endl;
cout << "result:" << res.result << endl;
}
return 0;
}