理解源IP地址和目的IP地址
IP地址(公网IP)唯一标识互联网中的一台主机
源IP,目的IP:对一个报文来说,从哪来,到哪里去。(Mac地址的变化)
最大的意义:知道一个报文如何进行路径选择
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,
但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析
端口号(port)是传输层协议的内容
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
pid(进程号):是系统分配给么一个进程的唯一标识符。PID就是各进程的身份标识符,程序一运行系统就会自动分配给进程一个独一无二的PID。进程终止后,PID被系统回收,可能会被继续给新运行的程序。
我们之前在初识进程中知道,单个计算机进程是用进程标示符(PID)标志的。但是在互联网的大环境下,操作系统很多,不同的操作系统有不同的进程标识符,所以仅仅用进程标示符是不足够的。因此,为了让不同操作系统的计算机应用程序能够互相通信,就必须用统一的方法对进程进行标志但就算使用统一的标示符进行标识,也存在问题
1.进程的创建和撤销是动态的,通信的一方几乎无法识别对方的进程
2.我们需要主机提供的功能来识别通信的重点,但是我们无法识别具体的进程是哪个
所以:运输层使用“”协议端口号“来解决这个问题,就是端口号。端口号解决了传输层的分用问题
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论
们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
socket()函数的原型如下,这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个标识这个套接字的文件描述符,失败的时候返回-1。
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
recvfrom函数用于从(已连接)套接口上接收数据,并捕获数据发送源的地址。
函数说明:sendto() 用来将数据由指定的socket 传给对方主机.
popen()
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
sockaddr_in 结构
sockaddr_in在头文件#include
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
思考:
udp_server.cc
#include
#include
#include
#include
#include
#include
#include
#include
//const uint16_t port = 8080;
void Usage(std::string proc)
{
std::cout << "Usage \n\t" << proc << " server_port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 0;
}
uint16_t port = atoi(argv[1]);
//创建套接字,打开网络文件
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "socket create error" << errno << std::endl;
return 1;
}
//2.服务器绑定端口和IP
//服务器的IP加port必须被客户知道
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);//此处的端口号,是我们计算机上的变量,是主机序列
// a.需要将人识别的点分十进制,字符串风格的IP地址转换成为4字节的整形IP
// b.也需要考虑大小端
// in_addr_t inet_addr (const char *__cp) 能完成上面两个工作
// 云服务器,不允许用户直接绑定公网IP,另外,我们正常编写的时候也不会指明IP
//local.sin_addr.s_addr = inet_addr("43.431.43.43")//
//INADDR_ANY 如果你绑定的是确定的IP(主机),意味着只有发到该IP主机上的数据才会交给你的网络进程
//但是,一般的服务器可能有多张网卡,配置多个IP,我们需要的不是某个IP上的数据,而是所有发送到该主机上的数据
local.sin_addr.s_addr = INADDR_ANY; //#define INADDR_ANY ((in_addr_t) 0x00000000)
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind error" << errno << std::endl;
return 2;
}
// 提供服务
bool quit = false;
#define NUM 1024
char buffer[NUM];
while(!quit)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if(cnt > 0)
{
buffer[cnt] = 0;
FILE* fp = popen(buffer, "r");
if(fp == nullptr)
{
std::cout << "open failed" << std::endl;
return 3;
}
std::string s;
char msg[1024] = { 0 };
while(fgets(msg, sizeof(msg), fp) != nullptr);
{
s += msg;
}
sendto(sock, s.c_str(), s.size(), 0, (struct sockaddr*)&peer, len);
// char msg[1024] = { 0 };
// size_t number = fread(msg, sizeof(char), strlen(msg), fp);
// if(number > 0)
// {
// msg[number] = 0;
// std::cout << "client: " << buffer << std::endl;
// //std::string s("server say hello");
// //sendto(sock, s.c_str(), s.size(), 0, (struct sockaddr*)&peer, len);
// sendto(sock, msg, strlen(msg), 0, (struct sockaddr*)&peer, len);
// }
// else
// {
// // TODO
// }
pclose(fp);
}
else
{
// TODO
}
}
return 0;
}
udp_client.cc
#include
#include
#include
#include
#include
#include
void Usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << "server_ip server_port" << std::endl;
}
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)
{
std::cerr << "socket errno: " << errno << std::endl;
return 1;
}
// 客户端必须要有IP和port
// 但是客户端不需要显示的绑定,一旦显示bind,就必须明确client要和哪一个port关联
// client正常发送数据的时候,一般由操作系统自动绑定,采用的是随机绑定的方式
// 2.使用服务
// 你的要给谁发
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]);
bool quit = false;
while(!quit)
{
// 你的数据从哪里来
std::string message;
//std::cout << "输入#" << std::endl;
std::cout << "MyShell:";
getline(std::cin, message);
//向服务端发送信息
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
char buffer[1024];
ssize_t cnt = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&tmp, &len);
if(cnt > 0)
{
buffer[cnt] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
{
// TODO
}
}
return 0;
}
Task.hpp
#pragma once
#include
#include
#include
#include
#include
namespace ns_task
{
class Task
{
public:
Task()
: _sock(-1)
{
}
Task(int sock)
:_sock(sock)
{
}
~Task()
{
}
int operator()()
{
return Run();
}
public:
int Run()
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer) - 1);
ssize_t cnt = read(_sock, buffer, sizeof(buffer) - 1);
if (cnt > 0)
{
buffer[cnt] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string s;
s += "server ";
s += buffer;
write(_sock, s.c_str(), s.size());
}
else if (cnt == 0)
{
std::cout << "client quit..." << std::endl;
}
else
{
std::cerr << "read error" << std::endl;
}
close(_sock);
}
private:
int _sock;
};
}
ThreadPool.hpp
#pragma once
#include
#include
#include
#include
namespace ns_task_queue
{
int g_num_default = 10;
template <class T>
class ThreadPool
{
private:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void UnLock()
{
pthread_mutex_unlock(&_mtx);
}
bool IsEmpty()
{
return _tq.empty();
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
void WakeUp()
{
pthread_cond_signal(&_cond);
}
ThreadPool(int num = g_num_default)
: _num(g_num_default)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool<T> &tp) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
public:
static ThreadPool<T> *GetInstance()
{
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//对象还没有被创建
if (ins == nullptr)
{
pthread_mutex_lock(&lock);
if (ins == nullptr)
{
ins = new ThreadPool<T>;
ins->InitThreadPool();
}
pthread_mutex_unlock(&lock);
}
return ins;
}
~ThreadPool()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
void PushTask(const T &in)
{
Lock();
_tq.push(in);
UnLock();
WakeUp();
}
void PopTask(T *out)
{
*out = _tq.front();
_tq.pop();
}
//加上static防止this指针,Rountine只有一个参数
static void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tq = (ThreadPool<T> *)args;
while (true)
{
// std::cout << "thread running... 线程是:" << pthread_self() << std::endl;
T t;
tq->Lock();
while (tq->IsEmpty())
{
tq->Wait();
}
tq->PopTask(&t);
tq->UnLock();
t();
}
}
void InitThreadPool()
{
pthread_t tid[_num];
for (int i = 0; i < _num; ++i)
{
pthread_create(tid + 1, nullptr, Rountine, this);
}
}
private:
int _num; //线程数目
std::queue<T> _tq; //任务队列,临界资源
pthread_mutex_t _mtx;
pthread_cond_t _cond;
static ThreadPool<T> *ins;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
tcp_client.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// tcp_client server_ip server_port
void Usage(std::string proc)
{
std::cout << "Usage\n\t" << proc << "server_ip " << "server_port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string srv_ip = argv[1];
uint16_t srv_port = static_cast<uint16_t>(atoi(argv[2]));
// 创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 2.client不需要显示的绑定, 也不需要listen和accept
// client需要connect服务器
struct sockaddr_in server;
// The bzero() function sets the first n bytes of the area starting at s to zero (bytes containing '\0')
bzero(&server, sizeof(server));//一般用memset
server.sin_family = AF_INET;
// 函数作用有两个,将点分十进制转换为4字节IP,将4字节的主机序列转换为网络序列
server.sin_addr.s_addr = inet_addr(srv_ip.c_str());
// 16位主机序列转换为网络序列
server.sin_port = htons(srv_port);
if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
{
std::cerr << "connect error" << std::endl;
return 3;
}
std::cout << "connect success!" << std::endl;
// 3, 进行业务请求了
while(true)
{
std::string buffer;
std::cout << "please Enter#";
getline(std::cin, buffer);
write(sock, buffer.c_str(), buffer.size());
char ret[1024];
bzero(ret, sizeof(ret));
ssize_t cnt = read(sock, ret, sizeof(ret));
if(cnt > 0)
{
ret[cnt] = 0;
std::cout << "server# " << ret << std::endl;
}
}
return 0;
}
tcp_server.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "ThreadPool.hpp"
#include "Task.hpp"
void Usage(std::string proc)
{
std::cout << "Usage\n\t" << proc << ":server_port" << std::endl;
}
void ServiceIO(int new_sock)
{
while(true)
{
char buffer[1024];
memset(buffer, 0, sizeof(buffer) - 1);
ssize_t cnt = read(new_sock, buffer, sizeof(buffer)-1);
if(cnt > 0)
{
buffer[cnt] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string s;
s += "server ";
s += buffer;
write(new_sock, s.c_str(), s.size());
}
else if(cnt == 0)
{
std::cout << "client quit..." << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
}
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
uint16_t port = static_cast<uint16_t> (atoi(argv[1]));
// 1.创建套接字, tcp
// On success, a file descriptor for the new socket is returned.
// On error, -1 is returned, and errno is set appropriately.
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socket failed" << errno << std::endl;
return 2;
}
// 2.bind 绑定
// 头文件#include and #include
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);// 端口,在网络中是大端
if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
std::cerr << "bind failed" << errno << std::endl;
return 3;
}
// 因为tcp是面向连接的, 在通信前,需要先建立连接,才能通信
// 一定有人需要主动连接(客户端,需要服务),有人需要被动连接(服务器,提供服务)
// 3.设置套接字是listen状态,,本质上是允许用户连接
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
std::cerr << "listen failed" << errno << std::endl;
}
//在Linux中父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源
// signal(SIGCHLD, SIG_IGN);
// 4. 接受
for(; ; )
{
struct sockaddr_in perr;
socklen_t len = sizeof(perr);
int new_sock = accept(listen_sock, (sockaddr*)&perr, &len);
if(new_sock < 0)
continue;
u_int16_t client_port = ntohs(perr.sin_port);
std::string client_ip = inet_ntoa(perr.sin_addr);
std::cout << "get a new link->[" << client_ip << ":" << client_port << "]#" << new_sock << std::endl;
// version4
// 线程池或则进程池
// 构建任务
ns_task::Task t(new_sock);
// 将任务push到线程池即可
ns_task_queue::ThreadPool<ns_task::Task>::GetInstance()->PushTask(t);
// version3
// 创建线程或则进程无上西限
// std::thread t1([new_sock]
// {
// ServiceIO(new_sock);
// close(new_sock);
// });
// t1.detach();
// std::cout << "新线程分离成功!" << std::endl;
// version 1,单进程没人使用
//ServiceIO(new_sock);
//version2, 多进程
//pid_t pid = fork();
// if(pid < 0)
// {
// continue;
// }
// else if(pid == 0) // 曾经被父进程打开的fd,也会被继承
// {
// // child
// close(listen_sock);
// if(fork() > 0)
// {
// exit(0); // 父进程终止,
// }
// else if(fork() == 0)
// {
// // 下面的是孙子进程, 父进程终止,孙子进程变为孤儿进程,由操作系统管理
// ServiceIO(new_sock);
// close(new_sock);
// exit(0);
// }
// }
// else
// {
// // father
// close(new_sock);
// waitpid(pid, 0, 0);
// }
}
return 0;
}
下图是基于TCP协议的客户端/服务器程序的一般流程:
服务器初始化:
建立连接的过程:
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
断开连接的过程:
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的: