一、预备知识
理解源IP地址和目的IP地址
IP地址(公网IP)唯一标识互联网中的一台主机 源IP,目的IP:对一个报文来说,从哪来,到哪里去。(Mac地址的变化) 最大的意义:知道一个报文如何进行路径选择
思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析
二、认识端口号
端口号(port)是传输层协议的内容
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
三、理解 “端口号” 和 “进程ID”
我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这两者之间是怎样的关系?
pid(进程号):是系统分配给么一个进程的唯一标识符。PID就是各进程的身份标识符,程序一运行系统就会自动分配给进程一个独一无二的PID。进程终止后,PID被系统回收,可能会被继续给新运行的程序。
我们之前在初识进程中知道,单个计算机进程是用进程标示符(PID)标志的。但是在互联网的大环境下,操作系统很多,不同的操作系统有不同的进程标识符,所以仅仅用进程标示符是不足够的。因此,为了让不同操作系统的计算机应用程序能够互相通信,就必须用统一的方法对进程进行标志但就算使用统一的标示符进行标识,也存在问题
1.进程的创建和撤销是动态的,通信的一方几乎无法识别对方的进程 2.我们需要主机提供的功能来识别通信的重点,但是我们无法识别具体的进程是哪个 所以:运输层使用“”协议端口号“来解决这个问题,就是端口号。端口号解决了传输层的分用问题
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;
四、认识TCP协议和UDP协议
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论
五、网络字节序
们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
- 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
六、socket编程接口
6.1 socket 常见API
创建 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()
6.2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
sockaddr 结构
sockaddr_in 结构 sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义,该结构体解决了sockaddr的缺陷,把port和addr 分开储存在两个变量中,如下:
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址. in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
6.3 地址转换函数
本节只介绍基于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把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果
思考:
- 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
- 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
- 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
- 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;
七、简单的UDP网络程序
udp_server.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cerrno>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
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;
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
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);
pclose(fp);
}
else
{
}
}
return 0;
}
udp_client.cc
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
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;
}
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if(sock < 0)
{
std::cerr << "socket errno: " << errno << std::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]);
bool quit = false;
while(!quit)
{
std::string message;
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
{
}
}
return 0;
}
八、简单的TCP网络程序、
8.1 TCP socket API 详解
socket():
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
- protocol参数的介绍从略,指定为0即可
bind():
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
- struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结
构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
我们的程序中对myaddr参数是这样初始化的:
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑
定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用 哪个IP 地址; - 端口号为SERV_PORT, 我们定义为9999;
listen():
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究;
- listen()成功返回0,失败返回-1;
accept():
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
connect
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1
Task.hpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <cerrno>
#include <string>
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 <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
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 void *Rountine(void *args)
{
pthread_detach(pthread_self());
ThreadPool<T> *tq = (ThreadPool<T> *)args;
while (true)
{
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 <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstdlib>
#include <strings.h>
#include <unistd.h>
#include <string.h>
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;
}
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(srv_ip.c_str());
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;
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 <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <thread>
#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]));
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socket failed" << errno << std::endl;
return 2;
}
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;
}
const int back_log = 5;
if(listen(listen_sock, back_log) < 0)
{
std::cerr << "listen failed" << errno << std::endl;
}
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;
ns_task::Task t(new_sock);
ns_task_queue::ThreadPool<ns_task::Task>::GetInstance()->PushTask(t);
}
return 0;
}
8.2 TCP协议通讯流程
下图是基于TCP协议的客户端/服务器程序的一般流程: 服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程, 通常称为 三次握手;
数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
- 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
- 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
- read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
- 客户端收到FIN, 再返回一个ACK给服务器; (第四次)
这个断开连接的过程, 通常称为 四次挥手
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
|