目录
[TCP socket API 详解](#TCP socket API 详解)
[TCP 和 UDP 对比](#TCP 和 UDP 对比)
TCP socket API****详解
下面介绍程序中用到的 socket API, 这些函数都在 sys/socket.h 中。
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指定结构体的长度
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。
代码实现TCP通讯
服务端
cpp
using namespace std;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
extern Log lg;
const int backlog = 10; //积压
enum
{
UsageError = 1,
SocketError,
BindError,
ListenError,
};
class TcpServer
{
public:
TcpServer(const uint16_t port, const string ip = defaultip)
: _port(port)
, _ip(ip)
, _listensock(defaultfd)
{}
~TcpServer()
{
if (_listensock!= defaultfd)
{
close(_listensock);
}
}
void InitServer()
{
_listensock = socket(AF_INET, SOCK_STREAM, 0);
if (_listensock == -1)
{
lg(Fatal, "create socket, errno: %d, errstring: %s", errno, strerror(errno));
exit(SocketError);
}
lg(Info, "create socket success, listensock_: %d", _listensock);
int opt = 1;
setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启(tcp协议的时候再说)
struct sockaddr_in local;
memset(&local, 0, sizeof(local)); //bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(_ip.c_str());
local.sin_port = htons(_port);
if (bind(_listensock, (struct sockaddr*)&local, sizeof(local)) == -1)
{
lg(Fatal, "bind error, errno: %d, errstring: %s", errno, strerror(errno));
exit(BindError);
}
lg(Info, "bind socket success, listensock_: %d", _listensock);
// Tcp是面向连接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
if (listen(_listensock, backlog) == -1) //第二个参数用来设置最大积压
{
lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
exit(ListenError);
}
lg(Info, "listen socket success, listensock_: %d", _listensock);
}
void Start()
{
Daemon();
ThreadPool<Task>::GetInstance()->Start();
lg(Info, "tcpServer is running....");
for(;;)
{
//1.获取accsocket
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int accsock = accept(_listensock, (struct sockaddr*)&peer, &len);
if (accsock == -1)
{
lg(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //?
continue; //只是接受一次失败,继续接受下一个连接
}
uint16_t peerport = ntohs(peer.sin_port);
char clientip[32];
inet_ntop(AF_INET, &(peer.sin_addr), clientip, sizeof(clientip));
// 2. 根据新连接来进行通信
lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", accsock, clientip, peerport);
//线程池版本
Task t(accsock, clientip, peerport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
private:
int _listensock;
uint16_t _port;
string _ip;
};
注意
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
参数讲解
线程池版本的 TCP 服务器
由于TCP服务器可能被多个客户端去访问,因此可以优先创建一批线程池,让线程池去处理来自Client的需求。
客户端
cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(const std::string &proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// ./tcpclient serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
struct sockaddr_in server;
bzero(&server, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
while (true)
{
int cnt = 5; //重连次数
int isreconnect = false;
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 1;
}
//设置重连机制
do
{
// tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
// 客户端发起connect的时候,进行自动随机bind
int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
isreconnect = true; //设置重连标志
cnt--;
std::cerr << "connect error..., reconnect: " << cnt << std::endl;
sleep(2);
}
else
break;
}while (cnt && isreconnect); //重连次数为0,退出循环
if (cnt == 0)
{
std::cerr << "user offline..." << std::endl;
break;
}
string msg;
std::cout << "Please Enter# ";
std::getline(std::cin, msg);
// TCP在接受发送数据的时候使用的接口有send、write、read、recv
int n = write(sockfd, msg.c_str(), msg.size());
if (n < 0)
{
std::cerr << "write error..." << std::endl;
// break;
}
char inbuf[1024] = {0};
n = read(sockfd, inbuf, sizeof(inbuf));
if (n > 0)
{
inbuf[n] = 0;
std::cout << inbuf << std::endl;
}
else{
// break; 不选择退出,而是断线重连
}
close(sockfd); //每次循环都会新建一个套接字描述符,所以需要关闭套接字描述符
}
return 0;
}
Task
cpp
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
Init init;
extern Log lg;
class Task
{
public:
Task(int sockfd, const std::string &clientip, const uint16_t &clientport)
: sockfd_(sockfd), clientip_(clientip), clientport_(clientport)
{
}
Task()
{
}
void run()
{
// 测试代码
char buffer[4096];
// Tcp是面向字节流的,你怎么保证,你读取上来的数据,是"一个" "完整" 的报文呢?
ssize_t n = read(sockfd_, buffer, sizeof(buffer)); // BUG?
if (n > 0)
{
buffer[n] = 0;
std::cout << "client key# " << buffer << std::endl;
std::string echo_string = init.translation(buffer);
// sleep(5);
// // close(sockfd_);
// lg(Warning, "close sockfd %d done", sockfd_);
// sleep(2);
n = write(sockfd_, echo_string.c_str(), echo_string.size()); // 100 fd 不存在
if(n < 0)
{
lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));
}
}
else if (n == 0)
{
lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);
}
else
{
lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);
}
close(sockfd_);
}
void operator()()
{
run();
}
~Task()
{
}
private:
int sockfd_;
std::string clientip_;
uint16_t clientport_;
};
守护进程
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";
//1.忽略信号:SIGCHLD, SIGPIPE, SIGSTOP 2.独立会话 3.更改工作目录 4.重定向
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);
}
}
守护进程的几大关键:1.忽略信号(子进程会继承父进程的信号处理) 2.设置umask 3.创建子进程,独立会话 4.进行重定向到/dev/null
守护进程
守护进程(daemon)是一种在后台运行的进程,通常没有控制终端,它们执行系统任务,不需要用户交互。守护进程通常在系统启动时启动,并在系统关闭时终止。以下是一些为什么守护进程需要忽略某些信号的原因:
-
**防止意外终止**:守护进程通常执行关键任务,如网络服务、数据备份等。如果守护进程响应了某些信号(如SIGINT,通常由Ctrl+C产生),它们可能会被意外终止,这会影响系统的稳定性和服务的可用性。
-
**避免不必要的重启**:某些信号(如SIGHUP)默认情况下会导致进程重启。对于守护进程来说,这通常不是期望的行为,因为它们通常设计为长期运行,而不是需要定期重启。
-
**防止子进程成为僵尸进程**:守护进程可能会创建子进程来执行任务。如果子进程终止,而守护进程没有正确处理SIGCHLD信号,子进程可能会变成僵尸进程,占用系统资源。
-
**保持服务的连续性**:守护进程忽略某些信号可以确保它们不会因为信号而中断正在执行的任务,从而保持服务的连续性和可靠性。
前台与后台
一般来讲,谁拥有键盘文件(终端操作,如 Ctrl + C),谁就是前台进程。
前后台之间的指令操作
1.command + &
后台任务执行的时候,会给你一个后台任务号与pid
2.jobs查看任务
3.fg + 后台任务号 将后台任务提到前台
^C可以终止前台的2号任务
4. 暂停程序并放到后台 :Ctrl + Z
暂停某个任务,将这个任务放到后台,shell重新回到前台
5. bg(back ground) + 任务 :把因为暂停放到后台的任务,在后台重新跑起来
Linux进程间关系
组ID是多个进程id的第一个(父进程)的pid
最开始这些进程的父进程都是bash,但是bash退出之后,这些进程就被托孤了,被OS领养
不受到会话的影响,即不受到bash的影响,也就是让进程的父进程变成OS,而不是bash,也就说,守护进程的本质还是孤儿进程
设置为独立会话setsid
谁调用这个函数,这个进程就会被设置为独立的回话(前提这个进程不能是进程组的组长)
怎么保证自己不是组长呢?---
父进程fork子进程之后,父进程直接退出,那么子进程就变成了了孤儿进程。孤儿进程默认是后台进程。
父进程关闭之后,其子进程的进程组ID(PGID)不会自动改变。进程组ID是在进程创建时确定的,并且会一直保持不变,除非显式地通过系统调用(如setpgid)来修改。
守护进程不受bash登录与退出的影响,因此bash退出时,关闭的0 1 2号文件需要关闭,为了不受他们的影响,应该重定向到一个垃圾桶文件---/dev/null
daemon接口
实际上系统实现了这个接口
允许将两个参数设置为0.
如果参数是0
第一个参数:改成根目录
第二个参数:把0 1 2重定向为垃圾桶
为什么需要设置umask
会话ID与组ID
在UNIX和类UNIX操作系统中**,每个进程都属于一个进程组,并且可以属于一个会话**。下面是组ID与会话ID的介绍:
### 组ID(Group ID,GID)
组ID是用于标识进程组 的一个整数。进程组是一组相关进程的集合 ,通常是由一个进程通过 `fork()` 系统调用创建的 。进程组的主要目的是允许信号被发送到一组进程,而不是单个进程。
-
**进程组ID(PGID)**:每个进程组都有一个唯一的进程组ID,它是该组中任一进程的组ID。通常,进程组ID是创建该组的第一个进程的进程ID。
-
**获取和设置PGID**:进程可以通过 `getpgrp()` 函数获取其进程组ID,通过**`setpgid()`**函数设置其进程组ID或另一个进程的进程组ID。
### 会话ID(Session ID)
会话ID是用于标识会话的一个整数。会话是一个或多个进程组的集合,它们是由进程通过 `setsid()` 系统调用创建的,用于实现进程的独立性。
- **会话**:会话通常是由控制终端(如用户登录的终端)启动的第一个进程创建的。会话中的所有进程组共享同一个会话ID。
-
**会话领导进程 **:创建会话的进程称为会话领导进程 。会话领导进程的进程ID同时也是会话ID。
-
**控制终端 **:会话可以有一个控制终端 。当会话领导进程打开一个终端时,该终端成为会话的控制终端。会话中的进程组可以接收来自控制终端的信号,如中断信号(SIGINT)。
### 关系和特性
-
一个会话可以包含多个进程组。
-
每个进程组只能属于一个会话。
-
会话领导进程不能成为另一个会话的成员。
-
如果会话领导进程终止,会话中的所有进程都不会受到影响,但会话可能失去控制终端。
-
如果会话没有进程了,会话会被销毁。
### 常用函数
-
`getsid()`:获取调用进程的会话ID。
-
`setsid()`:创建一个新的会话,并将调用进程设置为会话领导进程。
组ID与会话ID是UNIX系统中的基本概念,它们用于进程管理,特别是在信号处理和终端控制方面。了解这些概念对于编写多进程程序和守护进程至关重要。
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段
TCP 和 UDP 对比
可靠传输 vs 不可靠传输
有连接 vs 无连接
字节流 vs 数据报