本文只介绍基于IPv4的socket网络编程
端口号
一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
认识TCP协议
此处我们先对**TCP(Transmission Control Protocol 传输控制协议)**有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识UDP协议
此处我们也是对**UDP(User Datagram Protocol 用户数据报协议)**有一个直观的认识, 后面再详细讨论.
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
套接字编程种类
- 域间套接字编程---同一机器内
- 原始套接字编程---网络工具
- 网络套接字编程---用户间的网络通信
我们这里讲网络套接字编程
socket 常见API
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而, 各种网络协议的地址格式并不相同.
Pv4地址用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 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.
in_addr结构
socket编程接口
socket (TCP/UDP, 客户端 + 服务器)
创建套接字,返回值是网络文件描述符 (TCP/UDP, 客户端 + 服务器)
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符
参数:
domain: 创建套接字的域,我们这里填AF_INET表示使用ipv4的网络协议
**type:**套接字对应的类型,SOCK_STREAM字节流套接子,SOCK_DGRAM数据报套接字,就是TCP协议是面向字节流的,UDP协议是面向数据报的
**protocol:**协议类型,我们这里不用理,设为0先
返回值,就是网络文件描述符
bind (TCP/UDP, 服务器)
绑定套接字和端口号
绑定的IP地址和端口号都在addr里,这个结构体需要我们自己设置,再传进去
伪代码:
struct sockaddr_in
{
sin_family;//创建套接字的域 AF_INET
sin_port;//端口号
sin_addr;//IP地址
};
我们的程序中对addr参数是这样初始化的
- 将整个结构体清零;
- 设置地址类型为AF_INET;
- 网络地址为INADDR_ANY(0), 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
- 端口号为SERV_PORT, 我们定义为9999
recvfrom (UDP,服务器+客户端)
接收发给套接字的数据报信息
参数很好理解,flags设为0,后面的结构体是输出型参数,记录是谁发的
send (UDP,服务器和客户端)
向套接字发送数据报信息
listen (TCP,服务器)
listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节后面文章有
Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种一直在等待连接到来的状态,设置套接字为listen状态,这样就可以接受外来的连接
accept (TCP,服务器)
接收连接
- 三次握手完成后, 服务器调用accept()接受连接
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号
- 如果给addr 参数传NULL,表示不关心客户端的地址
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)
- 返回的是新的文件描述符,TCP服务器就是通过这个描述符来进行通信的,socket创建的文件描述符用来listen的
connect (TCP,客户端)
建立连接
- 客户端需要调用connect()连接服务器
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址
地址转换函数
本文只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP 地址但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示 和in_addr表示之间转换
字符串转in_addr的函数
in_addr转字符串的函数
关于inet_ntoa
inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动释放呢?
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
运行结果如下:因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数,但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题
简单的UDP网络程序
UDP(User Datagram Protocol 用户数据报协议):
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
用到的socket编程接口:
- socket,创建套接字
- bind,绑定套接字
- recvfrom,发送数据报
- send,接收数据报
一个关于IP地址
当你使用的是云服务器,udp服务端绑定公网IP时,禁止绑定,因为我们看到的是虚拟的IP地址,一般绑定0.0.0.0的IP地址表示绑定任意地址的IP地址,凡是发给我服务端的数据,都要根据端口号向上交付。我们也不建议绑定固定地址,当你的主机有多个网卡和IP地址时,假如绑定的是固定的IP,服务器接受数据是只会接受这个IP地址的数据,其他的就接收不了
一个关于端口号port
端口号不是任意绑定的,[0 ,1023]:系统内定的端口号,一般都要有固定的应用层协议使用,比如http是80,https是443,mysql是3306,这个是例外。所以我们绑定端口号时,绑定1024+以上
netstat -nlup 查看UDP程序
这个是我的Gitee,UDP网络程序的代码都在里面
实现的结果:一个简易的无界面的聊天服务器,每来一个人来访问,服务器就添加一个用户,服务器里的用户可以一起聊天,实现群聊
UDP服务端代码
编程思路:
- 创建套接字
- 绑定IP和端口号
- 接收消息,添加用户
- 发送信息,向每一个添加的用户发消息
udpserver.hpp
cpp
#pragma once
#include <iostream>
#include <cstring>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
// typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;
std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
const int size = 1024;
Log log;
enum
{
SOCKET_ERR = 1,
BIND_ERR,
};
class UdpServer
{
public:
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : ip_(ip), port_(port), isrunning(false)
{
}
void Init()
{
// 1.创建udp socket
// Udp 的socket是全双工的,允许被同时读写的
socketfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (socketfd_ < 0)
{
log(Fatal, "create socket erro,socketfd:%d", socketfd_);
exit(SOCKET_ERR);
}
log(Info, "socket create success, socketfd: %d", socketfd_);
// 2.绑定socket
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()); // 1. string -> uint32_t 2. uint32_t必须是网络序列的
// local.sin_addr.s_addr=htonl(INADDR_ANY);//或者这样
if (bind(socketfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
exit(BIND_ERR);
}
log(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
}
void ChectUser(const struct sockaddr_in &client, uint16_t clientport, const std::string &clientip)
{
auto it = online_user_.find(clientip);
if (it == online_user_.end())
{
online_user_.insert({clientip, client});
std::cout << "[" << clientip << ":" << clientport << "]" << "add to online user" << std::endl;
}
}
void BroadCast(const std::string &info, uint16_t clientport, const std::string &clientip)
{
for (const auto &users : online_user_)
{
std::string massege = "[";
massege += clientip;
massege += ":";
massege += to_string(clientport);
massege += "]# ";
massege += info;
socklen_t len = sizeof(users.second);
sendto(socketfd_, massege.c_str(), massege.size(), 0, (struct sockaddr *)&users.second, len);
}
}
void Run()
{
isrunning = true;
char buff[size];
while (true)
{
memset(buff,0,size);
struct sockaddr_in client;
socklen_t len = sizeof(client);
ssize_t n = recvfrom(socketfd_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&client, &len);
if (n < 0)
{
log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
std::string clientip = inet_ntoa(client.sin_addr);
ChectUser(client, clientport, clientip);
std::string info = buff;
BroadCast(info, clientport, clientip);
}
}
~UdpServer()
{
}
private:
int socketfd_; // 网络文件描述符
std::string ip_; // 绑定的IP地址
uint16_t port_; // 绑定的端口号
bool isrunning;
std::unordered_map<std::string, struct sockaddr_in> online_user_; // 存在线用户
};
main.cc 运行服务端
cpp
#include "udpserver.hpp"
#include <memory>
#include <cstdio>
#include <vector>
#include <functional>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(0);
}
uint16_t port = std::stoi(argv[1]);
// std::unique_ptr<UdpServer> svr(new UdpServer(port));
UdpServer *svr = new UdpServer(port);
svr->Init();
svr->Run();
return 0;
}
UDP客户端代码
编程思路:
-
创建套接字
-
client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
系统什么时候给我bind呢?首次发送数据的时候
3.创建两个线程分别进行收发数据
cpp
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void Usage(const std::string proc)
{
std::cout << "\n\tUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
struct ThreadData
{
std::string serverip;
struct sockaddr_in server;
int sockfd;
};
void *Recv_massege(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
char buff[1024];
while (true)
{
memset(buff, 0, sizeof(buff));
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t n = recvfrom(td->sockfd, buff, sizeof(buff), 0, (struct sockaddr *)(&tmp), &len);
if (n > 0)
{
buff[n] = 0;
std::cerr<< buff << std::endl;
}
}
}
void *Send_massege(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
std::string massege;
socklen_t len = sizeof(td->server);
while (true)
{
std::cout << "Please Enter@ ";
getline(std::cin, massege);
// 1. 数据 2. 给谁发
sendto(td->sockfd, massege.c_str(), massege.size(), 0, (struct sockaddr *)&(td->server), len);
}
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
ThreadData td;
bzero(&td.server, sizeof(td.server));
td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
td.server.sin_family = AF_INET;
td.server.sin_port = htons(serverport);
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0)
{
std::cout << "sock erro" << std::endl;
exit(1);
}
td.serverip = serverip;
// client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
// 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
// 系统什么时候给我bind呢?首次发送数据的时候
// 创建两个线程,分别收发数据
pthread_t recv_tid, send_tid;
pthread_create(&recv_tid, nullptr, Recv_massege, &td);
pthread_create(&send_tid, nullptr, Send_massege, &td);
pthread_join(recv_tid, nullptr);
pthread_join(send_tid, nullptr);
return 0;
}
结果就是这样,大概就是实现一个群聊,大家都访问这个服务器,就可以一起聊天
可能大家看不懂这里重定向
系统根目录下有个 /dev/pts/这个目录,这个目录里面是会话文件,你新起一个会话,就增加一个会话文件
你可以通过重定向字符串来知道哪个会话文件是哪个会话
守护进程
前台和后台进程
进程分为前台 进程和后台进程,每有一个用户登录Linux系统时,都会生成一个会话,每一个会话都会有个前台进程,bash进程,这个进程为我们提供了命令行解释,一个会话只能有一个前台进程,多个后台进程
当我们运行我们程序在前台时,bash进程就会变成后台,所以那时就不能进行命令行解释。前台进程和后台进程都可以向显示器打印,但只有前台进程拥有键盘文件进行标准输入,进程在后台了,键盘信号也接收不到了
后台进程相关指令:
- 运行程序+&:变成后台进程
- jobs:查看后台进程
- fa+后台任务号:把后台变成前台进程
- bg+后台任务号:把停止的后台进程启动,启动后还是后台进程
Linux进程间关系
守护进程原理
假如运行有后台进程,把所以会话关闭,你再次登录时发现这个后台进程还在的,虽然这个后台进程还在,但是会受到用户登录和退出的影响
如果你想让一个进程不想受任何用户登录和注销的影响,那就把进程变成守护进程,守护进程是自成进程组自成会话的进程,怎么做呢?
函数 setsid
把进程自成进程组自成会话,但是进程组的组长不能setsid,只能组内其他进程,所以我们可以进行创建子进程,直接让父进程直接退出,让子进程setsid
cpp
#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string nullfile = "/dev/null";
void Daemon(const std::string &cwd = "")
{
// 1. 忽略其他异常信号
signal(SIGCLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2. 将自己变成独立的会话
if (fork() > 0)
exit(0);
setsid();
// 3. 更改当前调用进程的工作目录
if (!cwd.empty())
chdir(cwd.c_str());
// 4. 标准输入,标准输出,标准错误重定向至/dev/null,这个文件相当于垃圾桶,把标准输入,标准输出,标准错误的信息往这个垃圾桶里丢弃
int fd = open(nullfile.c_str(), O_RDWR);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
函数 deamon 系统提供的守护进程化的接口
从man手册里可以知道他的两个参数就是上面我们自己写的功能
我们一般都是自己写,但还是可以用系统提供的
简单的TCP网络程序
netstat -nltp 查看TCP程序
TCP通信是全双工的
TCP(Transmission Control Protocol 传输控制协议):
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
用到的socket编程接口:
- socket,创建套接字
- bind,绑定套接字
- listen,监听
- accept,接收连接
- connect,发起连接
这个是我的Gitee,UDP网络程序的代码都在里面
实现的结果:服务器给用户提供翻译服务,并且守护进程化了
TCP服务端代码
编程思路:
- 创建套接字
- 绑定IP和端口号
- 开始监听
- 接收连接
- 根据新连接来通信---代码里提供了多种版本
下面是主要的代码文件,其余文件都在上面的Gitee里
tcpserver.hpp 服务端
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "task.hpp"
#include "log.hpp"
#include "threadpool.hpp"
#include "daemon.hpp"
extern Log log;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
enum
{
SocketErr = 1,
BindErr,
ListenErr,
};
class TcpServer;
class ThreadData
{
public:
ThreadData(TcpServer *tser, int sockfd, const std::string &ip, const uint16_t &port) : tser_(tser), sockfd_(sockfd), clientip_(ip), clientport_(port)
{
}
public:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
TcpServer *tser_;
};
class TcpServer
{
public:
TcpServer(const uint16_t &port, const std::string &ip = defaultip) : port_(port), ip_(ip)
{
}
void Init()
{
// 1.创建socket
listensock_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensock_ < 0)
{
log(Fatal, "socket err,errno: %d,errstring: %s", errno, strerror(errno));
exit(SocketErr);
}
log(Info, "socket success,listensock: %d", listensock_);
// 2.bind IP和端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
inet_aton(ip_.c_str(), &(local.sin_addr));
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
log(Fatal, "bind err,errno: %d,errstring: %s", errno, strerror(errno));
exit(BindErr);
}
log(Info, "bind success,listensock: %d", listensock_);
// 3.开始监听
// Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种一直在等待连接到来的状态
if (listen(listensock_, backlog) < 0)
{
log(Fatal, "listen err,errno: %d,errstring: %s", errno, strerror(errno));
exit(ListenErr);
}
log(Info, "listen success,listensock: %d", listensock_);
}
static void *Routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
td->tser_->Service(td->sockfd_, td->clientip_, td->clientport_);
delete td;
return nullptr;
}
void Run()
{
Daemon(); // 守护进程化
signal(SIGPIPE, SIG_IGN); // 把SIGPIPE忽略,防止服务器当读端关闭或者文件描述符关闭时收到这个信号导致服务器关闭
ThreadPool<Task>::GetInstance()->Start(); // version4 启动线程池
log(Info, "tcpserver is running");
while (true)
{
// 1.获取新连接
// signal(SIGCHLD,SIG_IGN);//下面多进程版本的进程等待,可以把SIGCHLD信号忽略,就不用写waitpid了
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sockfd = accept(listensock_, (struct sockaddr *)&client, &len); // 这个sockfd是用来发送和接收消息的
if (sockfd < 0)
{
log(Warning, "accept err,errno: %d,errstring: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = client.sin_port;
char clientip[32];
inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
// 2.根据新连接来通信
log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
// version1 单进程版---缺陷:当有多个用户访问时,其他用户会被阻塞
// Service(sockfd, clientip, clientport);
// close(sockfd);
// version2 多进程版---缺陷:创建进程耗资源,成本高
// pid_t id=fork();
// if(id==0)
// {
// //child
// close(listensock_);
// if(fork()>0) exit(0);
// Service(sockfd, clientip, clientport);//孙子进程,由1号进程领养
// close(sockfd);
// exit(0);
// }
// close(sockfd);
// pid_t rid=waitpid(id,nullptr,0);
// version3 多线程版---缺陷:每来一个用户都要为它创建线程,会导致线程有很多,也是不太有利的
// ThreadData* td=new ThreadData(this,sockfd,clientip,clientport);
// pthread_t tid;
// pthread_create(&tid,nullptr,Routine,td);
// version4 线程池版本---最优
Task t(sockfd, clientip, clientport);
// ThreadPool<Task>::GetInstance()->Start();//在最开始启动不是在这
ThreadPool<Task>::GetInstance()->Push(t);
}
}
void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
{
while (true)
{
char buff[4096];
int n = read(sockfd, buff, sizeof(buff));
if (n > 0)
{
buff[n] = 0;
std::cout << "client say@ " << buff << std::endl;
std::string massege = "tcpserver say@ ";
massege += buff;
write(sockfd, massege.c_str(), massege.size());
}
else if (n == 0)
{
log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
break;
}
else
{
log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
break;
}
}
}
~TcpServer()
{
}
private:
int listensock_;
uint16_t port_;
std::string ip_;
};
main.cc 运行服务端
cpp
#include <iostream>
#include <memory>
#include "tcpserver.hpp"
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
//./tcpserver 8080
int main(int argc,char* argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(1);
}
const uint16_t port=std::stoi(argv[1]);
std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
tcp_svr->Init();
tcp_svr->Run();
return 0;
}
TCP客户端代码
编程思路:
-
创建套接字
-
tcp客户端要不要bind?要。要不要显示的bind?不要。系统进行bind,随机端口。客户端发起connect的时候,进行自动随机bind
-
发起连接
-
实现假如服务器突然断开,重连的现象
tcpclient.cc 客户端
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << "serverip serverport" << std::endl;
}
//./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
const std::string serverip = argv[1];
const uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
socklen_t len = sizeof(server);
while (true)
{
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket err" << std::endl;
exit(1);
}
//实现重连
int cnt = 30;
bool isreconnect = false;
do
{
// tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&server, len);
if (n < 0)
{
isreconnect = true;
std::cerr << "connect err,isreconnect... cnt:" << cnt << std::endl;
cnt--;
sleep(1);
}
else
{
break;
}
} while (cnt && isreconnect);
if (cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
std::string massege;
std::cout << "Please Enter@ ";
std::getline(std::cin, massege);
int n = write(sockfd, massege.c_str(), massege.size());
if (n < 0)
{
std::cerr << "write err" << std::endl;
}
char buff[4096];
n = read(sockfd, buff, sizeof(buff));
if (n > 0)
{
buff[n] = 0;
std::cout << buff << std::endl;
}
close(sockfd);
}
return 0;
}
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给服务器 (第四次)
这个断开连接的过程, 通常称为四次挥手
TCP 和 UDP 对比
- 可靠传输 vs 不可靠传输
- 有连接 vs 无连接
- 字节流 vs 数据报