目录
[1. TCP socket API 详解](#1. TCP socket API 详解)
[1.1 socket](#1.1 socket)
[1.2 bind](#1.2 bind)
[1.3 listen](#1.3 listen)
[1.4 accept](#1.4 accept)
[1.5 read&&write](#1.5 read&&write)
[1.6 connect](#1.6 connect)
[1.7 recv](#1.7 recv)
[1.8 send](#1.8 send)
[1.9 popen](#1.9 popen)
[1.10 fgets](#1.10 fgets)
[2. EchoServer](#2. EchoServer)
[3. 多线程远程命令执行](#3. 多线程远程命令执行)
[4. 引入线程池版本翻译](#4. 引入线程池版本翻译)
[5. 验证TCP - windows作为client访问Linux](#5. 验证TCP - windows作为client访问Linux)
[6. connect的断线重连](#6. connect的断线重连)
1. TCP socket API 详解
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。
1.1 socket
cpp
// main socket
NAME
socket - create an endpoint for communication
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// domain:域,表示要进行网络通信,AF_INET
// type:套接字类型,TCP就填SOCK_STREM(流式套接字),UDP就填SOCK_DGRAM(数据包套接字)
// protocol:设置为0,默认就是TCP Socket
// 返回值
RETURN VALUE
On success, a file descriptor for the new socket is returned. On error, -1 is returned, and errno is set appropriately.
// 跟UDP一样,成功就是新的文件描述符,失败就返回-1
1.2 bind
cpp
// man bind
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 填充当前服务器本地的socket信息,文件信息和网络信息关联起来。
// 返回值
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
// 如果成功了0被返回,否则-1被返回
1.3 listen
TCP的有连接和UDP的无连接
UDP叫面向数据报,无连接的,TCP叫面向字节流,有连接的,怎么证明UDP是无连接的呢?
只要我客户端一启动,随时就可以向服务器发消息,所以我们直接创建套接字之后,直接sendto就可以向服务器发消息了,没有所谓的连接,而TCP叫做面向连接的协议,即有连接,所以我们对应的TCP,要面向连接,如果我们的客户端(c)连接服务器(s),也叫cs模式,如果c向s发起对应的请求,就先的建立连接,连接建立成功才能通信,这就是TCP的特点,而客户点要连服务器,是不是要求这个服务器随时随地等待被连接。所以TCP是建立连接的,一定会存在协商的过程,所以tcp需要将socket设置为监听状态。
cpp
// man listen
NAME
listen - listen for connections on a socket
// 意思就是再等别人随时随地来连接我
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// sockfd:设置的文件描述符
// backlog:服务器所对应的操作系统中所对应的全连接的个数,不建议为0,也不能太长,8,4,14,32都可以,具体设置多大,按照自己的需求来做,backlog是积压的意思,就类似于排队,但是排队也不能排太长
// 返回值
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
// 成功了返回0,失败了-1被返回
1.4 accept
cpp
// man accept
NAME
accept, accept4 - accept a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
// 从指定的文件描述符当中获取新连接,获取新的客户端,当别人连接你的时候,第一件是就是要知道谁来连我,
// sockfd:套接字
// addr && addrlen:输出型参数,其实就相当于给我们传进来缓冲区,当我们再进行获取新连接,这个上面所对应的sockaddr的客户端的地址就会直接给我们返回。这两个参数就等同于recvfrom,recvfrom的后两个参数。
// 返回值
RETURN VALUE
On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket. On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
// 如果成功就返回非0,失败-1返回。没有人连接就会阻塞。
// 成功后,这些系统调用将返回一个非负整数,该整数是所接受套接字的文件描述符,
// 正真提供服务的是由accept的返回值来定的。而sockfd是专门用来获取新连接的。
1.5 read&&write
读和写我们用的是read和write,因为我们的accept返回的是一个文件描述符,而且TCP是面向字节流的,管道和文件也是面向字节流的,所以TCP套接字,在我们获得了新的文件描述符之后,TCP套接字往后的处理都跟我们之前学习文件跟管道一样,我们可以调用read和write这样的文件接口来直接对网络进行读和写。
UDP不敢用read和write,因为UDP是面向数据报的,报文类型上是不一样的,至于什么是面向数据报,什么是面向字节流,单纯在应用层上编写代码是感受不到的,到TCP和UDP的原理时才能感受的到。
cpp
void HandlerRequest(int sockfd) // TCP也是全双工
{
char inbuffer[4096];
while(true)
{
size_t n = ::read(sockfd,inbuffer,sizeof(inbuffer)-1);
if(n > 0)
{
LOG(LogLevel::INFO) << inbuffer;
inbuffer[n] = 0;
std::string echo_str = "server echo# ";
echo_str += inbuffer;
::write(sockfd,echo_str.c_str(),echo_str.size());
}
}
}
void Start()
{
_isrunning = true;
while(_isrunning)
{
// 不能直接读数据
// 1. 获取新连接
struct sockaddr_in peer;
socklen_t peerlen;
int sockfd = ::accept(_listensockfd,CONV(&peer),&peerlen);
if(sockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);
continue;// 失败了不是报错,而是回头重新来
}
// 获取连接成功了
LOG(LogLevel::INFO) << "accept success,sockfd id : " << sockfd;
// version-0
HandlerRequest(sockfd);
}
_isrunning = false;
}
1.6 connect
cpp
// man connect
NAME
connect - initiate a connection on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// sockfd:客户端所创建的套接字
// addr && addrlen:访问目标服务器所对应的IP地址和端口号,
// 返回值
RETURN VALUE
If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropri‐ately.
// 创建套接字成功,返回值就是0,否则就是-1.
1.7 recv
cpp
// man recv
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 从指定的文件描述符读取数据到buffer,长度是len,flage就是读取的标志位,默认设置为0就可以了,因为在我们当前的代码中,设置为0就表示的是阻塞的,他比read就多了一个flags,这是专门在套接字当中为我们处理IO接口,
1.8 send
cpp
// man send
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 把特定缓冲区指定长度通过该文件描述符发出去,他也多了一个flags参数。
不管是read、write、recv还是end,他们的返回值的意思都是一样的,所以可以平滑过渡。
1.9 popen
cpp
// man popen
#include <stdio.h>
NAME
popen, pclose - pipe stream to or from a process
SYNOPSIS
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
// 它的底层其实就是帮我们做了这几件事情
/*
1. 执行 Shell 命令
启动一个新的子进程,运行指定的命令行程序(如 ls, grep, python 等)。
2. 建立单向管道
根据模式(读或写),创建一个管道:
读模式("r"):从子进程的输出(如 stdout)读取数据。
写模式("w"):向子进程的输入(如 stdin)写入数据。
3. 返回文件流指针
返回一个 FILE* 指针,可通过标准文件操作函数(如 fread, fgets, fprintf)与子进程交互。
*/
// 1. pipe
// 2. fork + dup2(pipe[1],1) + exec*,执行结果给父进程,pipe[0]
// 3. return
// 返回值是FILE*的,其实就是把管道封装起来,封装成一个文件了。
// 返回值
RETURN VALUE
popen(): on success, returns a pointer to an open stream that can be used to read or write to the pipe; if the fork(2) or pipe(2) calls fail, or if the function cannot allocate memory, NULL is returned.
// 成功的话就返回一个文件流,失败返回空
pclose(): on success, returns the exit status of the command; if wait4(2) returns an error, or some other error is detected, -1 is returned.
Both functions set errno to an appropriate value in the case of an error.
// 一个线程执行fork就相当于当前进程创建子进程。
1.10 fgets
cpp
// man fgets
NAME
fgetc, fgets, getc, getchar, ungetc - input of characters and strings
SYNOPSIS
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
// 返回值
RETURN VALUE
fgets() returns s on success, and NULL on error or when end of file occurs while no characters have been read.
// 读取失败返回NULL
2. EchoServer
3. 多线程远程命令执行
4. 引入线程池版本翻译
- 上篇UDP部分写过类似的设计方案。
- 后面我们还会涉及http相关内容,到时候在引入线程池会更方便,也很合理。
5. 验证TCP - windows作为client访问Linux

注意:一定要开放云服务器对应的端口号,在你的阿里云或者腾讯云、华为云的网站后台中开放。
我们可以发现tcp client(windows)和tcp server(Linux)可以通信。
WinSock2.h是Windows Sockets API(应用程序接口)的头文件,用于在Windows平台上进行网络编程。它包含了Windows Sockets 2(WinSock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets)进行网络通信。
在编写使用Winsock2的程序时,需要在源文件中包含WinSock2.h头文件。这样,编译器就能够识别并理解Winsock2中定义的数据类型和函数,从而能够正确的编译和链接网络相关的代码。
此外,与WinSock2.h头文件相对应的是ws2_32.lib库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用Winsock2 API中实现的函数。
在WinSock2.h中定义了一些重要的数据类型和函数,如:
WSADATA:保存初始化Winsock库时返回的信息。
SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。
sockaddr_in:IPv4地址结构体,用于存储IP地址和端口号等信息。
socket():创建一个套接字。
bind():将套接字与本地地址绑定。
listen():将套接字设置为监听模式,等待客户端的连接请求。
accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端
进行通信。
WSAStartup函数是Windows Sockets API的初始化函数,它用于初始化Winsock库。该函数在应用程序或DLL调用任何Windows套接字函数之前必须首先执行,它扮演着初始化的角色。
以下是WSAStartup函数的一些关键的:
它接受两个参数:wVersionRequested和IpWSAData。wVersionRequested用于指定所请求的Winsock版本,通常使用MAKEWORD(major,minor)宏,其中major和minor分别标识请求的主版本号和次版本号。IpWSAData是一个指向WSAData结构的指针用于接收初始化信息。
如果函数调用成功,它会返回0:否则,返回错误代码。
WSAStartup函数的主要作用是向操作系统说明我们将使用哪个版本的Winsock库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock库的状态会被初始化,应用程序就可以使用Winsock提供的一系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程序可以与底层的网络协议栈进行交互,实现网络通信。
在调用WSAStartup函数后,如果应用程序完成了对请求的Socket库的使用,应调用
WSAStartup函数后,如果应用程序完成了对请求的Socket库的使用,应调用WSACleanup
函数来解除与Socket库的绑定并释放所占用的系统资源。
6. connect的断线重连
客户端会面临服务器崩溃的情况,我们可以试着写一个客户端重连的代码,模拟并理解一些客户端行为,比如游戏客户端等。
采用状态机,实现一个简单的tcp client可以实现重连的效果。