目录
基本知识
通信两端
- 在网络通信程序中,通信两端被分为:
- 客户端:通常是提供给客户的通信端,是编写通信程序中主动发起请求的一端;
- 服务端:通常指被动接受请求,提供服务的通信端,在接受到客户端的请求之后,进行处理并返回;
- 客户端是主动发送请求的一端,这也就意味着客户端必须提前知道服务端的地址信息(IP + port),通常情况下,服务端的地址信息是固定的,并且提前提供(写入)客户端;
查看网络信息
netstat
:查看当前的网络状态信息;-a
:查看所有的网络信息;-t
:查看 TCP 网络信息;-u
:查看 UDP 网络信息;-n
:不以服务名称显示,也就是说将网络的地址信息显示出来,而不是以一些固定的服务名称显示;-p
:查看当前网络状态相对应的进程;
sockaddr
结构
概念
- socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,如 IPv4、IPv6,以及后面会学到的 UNIX Domain Socket 等等,这些网络协议的地址信息结构各不相同,但是却可以使用 socket API 来适用;
介绍
- 虽然不同协议底层的地址结构各不相同,但是地址结构体的前两个字节所存放的东西都是确定的,它所存放的就是地址域类型,IPv4、IPv6 地址域类型分别定义为宏 AF_INET、AF_INET6,这样只要取得某种
sockaddr
结构体的首地址,不需要知道具体是哪种类型的sockaddr
结构体,就可以根据地址域类型字段确定结构体中的内容; - IPv4 和 IPv6 的地址格式定义在
<netinet/in.h>
中,IPv4 地址用sockaddr_in
结构体表示,包括 16 位地址类型,16 位端口号和 32 位 IP 地址,以及 8 字节的填充位,IPv6 使用面较少,所以不做过多介绍; - 在接口的参数中,socket API 族的所有地址结构体都可以使用
struct sockaddr*
类型表示,在使用的时候只需要强制转化成所需的类型即可,如强转为sockaddr_in
,这样的好处是增加了程序的通用性,这样一来接口就可以接收 IPv4、IPv6 以及 UNIX Domain Socket 等各种类型的sockaddr
结构体指针做为参数,而不用写很多种接口,十分的方便;
- 下面来看看这些地址结构体的底层代码:
辅助接口介绍
字节序转换接口
- 下面是一些字节序转换接口,需要说明的是IP 地址的字节序转换只能用以 l 结尾的转换接口,端口的字节序转换只能用以 s 结尾的转换接口,这是因为他们的大小不一样,如果不使用对应的接口,那么可能会有数据的混乱;
uint32_t htonl(uint32_t hostlong);
:32 位数据,主机字节序到网络字节序的转换;uint16_t htons(uint16_t hostshort);
:16 位数据,主机字节序到网络字节序的转换;uint32_t ntonl(uint32_t netlong);
:32 位数据,网络字节序到主机字节序的转换;uint16_t ntons(uint32_t netlong);
:16 位数据,网络字节序到主机字节序的转换;
地址转换接口
下面这两个接口局限于 ipv4 的地址转换的;
in_addr_t inet_addr(const char* cp);
:将字符串点分十进制 IPv4 地址转换为整形的网络字节序 IPv4 地址,例如192.168.2.2 ——> 0xc0a80202
;char* inet_ntoa(struct in_addr in);
:将整型的网络字节序 IPv4 地址转换为字符串点分十进制 IPv4 地址;- 注意事项:这个接口返回了一个
char*
类型的数据,很显然是这个函数自己在内部为我们申请了一块内存来保存转换后的结果,那么这块内存是否需要我们手动释放呢?
man
手册上进行了说明:inet_ntoa
函数是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放,但是需要注意的是,如果多次调用这个函数,那么这块静态内存是会被覆盖的;
- 注意事项:这个接口返回了一个
下面这两个接口不局限于某一个协议的地址转换,只要指定地址域类型就可进行转换;
const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
:通用的转换接口,不局限于 IPv4,是将网络字节序二进制地址转换为字符串地址;af
:地址域类型,可以是AF_INET
、AF_INET6
等;scr
:指向待转换的网络字节序二进制地址的指针;dst
:指向转换后的字符串地址的指针;size
:地址域类型所对应的地址信息结构体的大小;
int inet_pton(int af, const char* src, void* dst);
:通用的转换接口,不局限于 IPv4,是将字符串地址转换为网络字节序二进制地址;af
:地址域类型;src
:指向待转换的字符串地址的指针;dst
:指向转换后的网络字节序二进制地址的指针;
UDP协议
概念
- 概念:UDP 协议又叫用户数据报协议,是在传输层的协议;
- 特性:无连接、不可靠传输、面向数据报;
- 应用场景:实时性要求大于安全性的要求的场景;
UDP通信流程
- 服务端流程
- 创建套接字,在内存中创建一个
socket
结构体; - 为套接字绑定地址信息(组织地址结构体),在创建套接字时,在内存中所创建的
socket
结构体中加入 IP + port 信息,加入 port 信息目的是为了告诉操作系统,主机接收到的很多数据中,哪些数据应该交给当前的这个socket
的接收缓冲区,加入 IP 地址信息是为了确定该套接字的源端地址信息; - 接收数据,客户端向服务端发送数据后,服务端根据这个数据的地址信息来确定将数据放到哪个套接字的接收缓冲区,然后进程再从与其绑定的固定端口号的
socket
接收缓冲区中取出数据,一般来说,服务端的地址信息是固定的,这样客户端才能准确地将信息发送给服务端,并且服务端也能准确地接收数据; - 发送数据,服务端在接收到数据后,根据数据中的信息确定要发往的对端地址,然后将要发送的数据放到
socket
的发送缓冲区中,内核选择合适的时候封装发送; - 关闭套接字,套接字是内核中的结构体,占据一定资源关闭套接字会释放内核中占用的资源;
- 创建套接字,在内存中创建一个
- 客户端流程
- 创建套接字,在内存中创建一个
socket
结构体; - 为套接字绑定地址,大部分情况下会忽略该步骤,因为当客户端将数据发送出去后,服务端就会拥有你的地址信息了,并且客户端的地址信息并不需要提供给谁,所以没有必要指定特定的地址信息;
- 发送数据,此时客户端已经确定了服务端的 IP + port,于是向固定的服务器发送数据,在发送时若
socket
没有绑定地址信息,则操作系统会选择合适的地址信息进行绑定; - 接收数据;
- 关闭套接字;
- 创建套接字,在内存中创建一个
UDP通信接口
创建套接字
int socket(int domain, int type, int protocol);
domain
:地址域类型,AF_INET
表示 IPV4,AF_INET6
表示 IPV6,AF_UNIX
表示本地通信;type
:SOCK_STREAM
——流式套接字,TCP 网络协议可用,SOCK_DGRAM
——数据报套接字,UDP 网络协议可用;protocol
:表示本次的通信协议,在头文件中是用宏来表示的,IPPROTO_TCP(宏:6)——TCP 协议,IPPROTO_UDP(宏:17)——UDP 协议;- 返回值:成功创建则返回套接字文件描述符——操作句柄,失败返回 -1;
绑定地址信息
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
sockfd
:创建套接字所返回的操作句柄;addr
:当前绑定的地址信息,也就是在传入参数之前,先对该地址结构体的成员赋值(赋值指:确定地址域类型、确定端口号、确定 IP 地址),然后在传参时强转为sockaddr*
类型;addrlen
:所绑定的地址信息的长度,也就是所使用的协议所对应的地址结构体的大小;- 返回值:成功返回 0,失败返回 -1;
接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flag, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd
:创建套接字所返回的操作句柄;buf
:存放接收到的数据的缓冲区指针len
:所要接收的数据长度;flag
:标志位,默认为 0——阻塞接收数据,缓冲区无数据则阻塞,MSG_DONTWAIT
——非阻塞接收,如果缓冲区没有数据则报错返回;src_addr
:获取源端地址信息,也就是通过类型强转获取到源端的地址结构体,然后从该地址结构体中拿到发送数据方的 IP + port;addrlen
:输入输出型参数,输入——指定要接收多大的对端地址信息结构体,输出——实际接收到的地址结构体大小;- 返回值:成功返回实际接收到的数据长度,失败返回 -1;
发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flag, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
:创建套接字所返回的操作句柄;buf
:要发送数据的缓冲区的首地址;len
:要发送数据的长度;flags
:标志位,默认为 0——阻塞发送,缓冲区满了则阻塞;dest_addr
:对端地址信息结构体,用来指定要将数据发送给谁;addrlen
:对端地址信息结构体的大小;- 返回值:成功返回实际发送的数据长度,失败返回 -1;
关闭套接字
int close(int sockfd);
sockfd
:创建套接字所返回的操作句柄;
实例
- 在实例过程中,我们在运行程序时,使用命令参数来为程序提供地址信息,例如
./test.exe 192.168.122.132 9000
;
使用C语言完成服务端功能
#include <stdio.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
//获取命令参数中的地址信息
if(argc != 3){
printf("Usage: ./main 192.168.122.132 9000\n");
return -1;
}
//创建UDP通信的套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(sockfd < 0){
perror("socket error\n");
return -1;
}
//组织一个IPV4的地址信息结构
struct sockaddr_in addr;
addr.sin_family = AF_INET; //地址域类型
addr.sin_port = htons(atoi(argv[2])); //将端口从主机字节序转换为网络字节序
//inet_addr将点分十进制ip地址转换为网络字节序ip地址
addr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(struct sockaddr_in);
//为服务端的套接字绑定地址信息
int ret = bind(sockfd, (struct sockaddr*)&addr, len);
if(ret < 0){
perror("bind error");
return -1;
}
//接下来就是开始循环接收数据、发送数据
while(1){
char buf[1024] = {0}; //接收数据的缓冲区
struct sockaddr_in cliaddr; //用来保存客户端的地址信息,后面发送数据要用到
socklen_t len = sizeof(struct sockaddr_in);
int ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&cliaddr, &len);
if(ret < 0){
perror("recvfrom error");
close(sockfd);
return -1;
}
printf("client say: %s\n", buf);
memset(buf, 0, 1024); //清空缓冲区内容,用来存放发送的数据
scanf("%s", buf);
len = sizeof(struct sockaddr_in);
//发送数据
ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&cliaddr, len);
if(ret < 0){
perror("sendto error");
close(sockfd);
return -1;
}
}
close(sockfd); //关闭套接字
}
使用C++对接口进行封装
- 下面对 UDP 通信流程进行封装,并用封装好的类来创建客户端程序;
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
using std::string;
class UdpSocket {
public:
//构造函数,这里其实只要对封装的套接字描述符进行初始化即可
UdpSocket()
:_sockfd(-1)
{}
~UdpSocket(){
Close();
}
//创建套接字
bool Socket(){
//创建IPV4网络通信的套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sockfd < 0){
std::cerr << "socket error" << std::endl;
return false;
}
return true;
}
//绑定本机ip地址及端口信息
bool Bind(const string& ip, uint16_t port){
//组织地址信息结构
struct sockaddr_in addr;
addr.sin_family = AF_INET; //地址域类型
addr.sin_port = htons(port); //绑定端口信息,需要主机字节序到网络字节序的转换
addr.sin_addr.s_addr = inet_addr(ip.c_str()); //绑定IP地址
socklen_t len = sizeof(struct sockaddr_in);
//绑定地址信息
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0){
std::cerr << "bind error" << std::endl;
return false;
}
return true;
}
//接收数据并获取对端的ip地址及端口信息
bool Recv(string& buf, string* ip = nullptr, uint16_t* port = nullptr){
//由于缓冲区的大小未知,缓冲区中的数据大小未知,所以我们先设置一块内存来接收数据,然后将接收的数据放到接收区中
char tmp[4096];
//组织地址信息结构
struct sockaddr_in peeraddr;
socklen_t len = sizeof(peeraddr);
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&peeraddr, &len);
if (ret < 0){
std::cerr << "recvfrom error" << std::endl;
return false;
}
//将数据放到接收区中
buf.assign(tmp, ret);
//如果不想获取数据发送方的地址信息,可以不传后面两个参数,默认为nullptr
if (ip != nullptr) {
*ip = inet_ntoa(addr.sin_addr);
}
if (port != nullptr) {
*port = ntohs(addr.sin_port);
}
return true;
}
//发送数据
bool Send(const string& data, const string& ip, const uint16_t& port){
//根据传入的参数来组织接收方的地址信息结构
struct sockaddr_in addr;
addr.sin_family = AF_INET; //IPV4地址域类型
addr.sin_port = htons(port); //端口从主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr(ip.c_str()); //IP地址从主机字节序转换为网络字节序
socklen_t len = sizeof(struct sockaddr_in);
//发送数据
int ret = sendto(_sockfd, &data[0], data.size(), 0, (struct sockaddr*)&addr, len);
if (ret < 0){
std::cerr << "send error" << std::endl;
return false;
}
return true;
}
//关闭套接字
bool Close(){
if (_sockfd >= 0){
close(_sockfd);
_sockfd = -1;
}
return true;
}
private:
//该类实例化的任何一个对象都是一个socket,所以我们封装一个套接字描述符
int _sockfd;
};
//接下来使用上面的类来创建一个客户端程序
//这里使用宏定义了一个返回值判断机制,如果返回值错误,则直接退出程序
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc, char* argv[]){
//接收命令参数,命令之后的两个参数分别为IP地址、端口号
if (argc != 3){
std::cerr << "./udp_cli serverip serverport" << std::endl;
return -1;
}
//创建客户端套接字对象
UdpSocket sock;
//创建套接字
CHECK_RET(sock.Socket());
//CHECK_RET(sock.Bind("192.168.11.128", 8000));
while (1){
//客户端先写入要发送的数据
std::cout << "client say: ";
string buf;
std::cin >> buf;
//发送数据
CHECK_RET(sock.Send(buf, argv[1], atoi(argv[2])));
buf.clear();
//对于服务端地址信息的接收可有可无,因为客户端是事先就知道服务端的地址信息的
string ip;
uint16_t port;
CHECK_RET(sock.Recv(buf, &ip, &port));
std::cout << "server say: " << buf << std::endl;
}
return 0;
}
TCP协议
概念
- 概念:TCP 协议又叫传输控制协议,是在传输层的协议;
- 特性:有连接、可靠传输、面向字节流;
- 应用场景:安全性要求大于实时性要求的场景;
TCP通信流程
- 服务端
- 创建套接字,这个套接字当做监听套接字使用;
- 为监听套接字绑定地址信息;
- 开始监听,将监听套接字置为 listen 状态,目的在于告诉系统可以处理客户端的连接请求了,从此刻开始,只要有一个新客户端连接请求,那么系统会为新客户端创建一个新的套接字,往后服务端与该客户端的通信就只靠这个套接字来完成;
- 获取新建连接,过程:利用监听套接字复制出新的套接字 s,然后为 s 绑定发送请求的客户端的地址信息,因为监听套接字原本就被绑定了服务端的地址信息了,所以 s 也拥有服务端的地址信息,这样一来,s 套接字就拥有了网络通信中的五元组(源端IP + 源端port + 对端IP + 对端port + TCP通信),此时就已完成建立连接了;
- 使用新的套接字收发数据;
- 一般关闭套接字的是新建的套接字,当服务端不在运行时,才会关闭监听套接字;
- 客户端
- 创建套接字;
- 为套接字绑定地址信息,一般不绑定,等进行通信时系统会自动寻找合适的地址信息绑定;
- 向服务端发起连接请求,需要事先知道服务端的地址信息;
- 收发数据;
- 关闭套接字;
TCP通信接口
创建套接字
int socket(int domain, int type, int protocol);
domain
:地址域类型,AF_INET
表示 IPV4,AF_INET6
表示 IPV6,AF_UNIX
表示本地通信;type
:SOCK_STREAM
——流式套接字,TCP 网络协议可用,SOCK_DGRAM
——数据报套接字,UDP 网络协议可用;protocol
:表示本次的通信协议,在头文件中是用宏来表示的,IPPROTO_TCP(宏:6)——TCP 协议,IPPROTO_UDP(宏:17)——UDP 协议;- 返回值:成功创建则返回套接字文件描述符——操作句柄,失败返回 -1;
绑定地址信息
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
sockfd
:创建套接字所返回的操作句柄;addr
:当前绑定的地址信息,也就是在传入参数之前,先对该地址结构体的成员赋值(赋值指:确定地址域类型、确定端口号、确定 IP 地址),然后在传参时强转为sockaddr*
类型;addrlen
:所绑定的地址信息的长度,也就是所使用的协议所对应的地址结构体的大小;- 返回值:成功返回 0,失败返回 -1;
服务端监听
int listen(int sockfd, int backlog);
sockfd
:前面创建的用来监听的套接字描述符;backlog
:服务端同一时间所能处理的最大连接数,在系统中有一个已连接节点队列,用来记录那些正连接着的套接字,但是资源有限,所以队列大小有限制,backlog
就是设置这个队列有多大;- 返回值:成功返回 0,失败返回 -1;
客户端请求连接
int connect(int sockfd, sockaddr* srvaddr, socklen_t addrlen);
sockfd
:客户端套接字描述符;srvaddr
:服务端地址信息;addrlen
:服务端地址信息大小;- 返回值:成功返回 0,失败返回 -1;
服务端新建连接
int accept(int sockfd, sockaddr* srcaddr, socklen_t* len);
sockfd
:监听套接字描述符;srcaddr
:输出型参数,用来获取客户端地址信息;len
:输入输出型参数,指定客户端地址信息长度,返回实际获取到的长度;- 返回值:返回新建立的套接字描述符,以后通信就靠该描述符,失败返回 -1;
接收数据
int recv(int sockfd, void* buf, int len, int flags);
sockfd
:当前套接字描述符;buf
:接收数据的缓冲区;len
:要接收数据的长度;flags
:默认 0,阻塞接收;- 返回值:实际接收字节数,失败返回 -1, 连接断开返回 0;
发送数据
int send(int sockfd, void* buf, int len, int flags);
sockfd
:当前套接字描述符;buf
:要发送数据的缓冲区;len
:要发送数据的长度;flags
:默认 0,阻塞发送;- 返回值:实际发送的字节数,失败返回 -1,连接主动断开方发送数据会触发 SIGPIPE 异常;
关闭套接字
int close(int sockfd);
sockfd
:创建套接字所返回的操作句柄;
实例
封装TCP程序流程
#include <cstdio>
#include <iostream>
#include <string>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
//用宏来进行函数返回值的检查,失败则直接退出程序
#define CHECK_RET(q) if((q)==false){return -1;}
#define LISTEN_BACKLOG 5 //TCP通信中,同一时间所能连接的最大数
class TcpSocket {
private:
//封装一个套接字描述符
int _sockfd;
public:
//构造函数
TcpSocket() :_sockfd(-1) {}
//创建套接字
bool Socket() {
//此处需要注意一下传输的方式
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0) {
perror("socket error");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const std::string& ip, const uint16_t port) {
//组织地址信息结构
sockaddr_in addr;
addr.sin_family = AF_INET; //IPV4地址域类型
addr.sin_port = htons(port); //端口从主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr(&ip[0]); //IP地址从主机字节序转换为网络字节序
socklen_t len = sizeof(sockaddr_in);
//绑定地址信息
int ret = bind(_sockfd, (sockaddr*)&addr, len);
if (ret < 0) {
perror("bind error");
return false;
}
return true;
}
//服务端开始监听
bool Listen(int backlog = LISTEN_BACKLOG) {
//listen(描述符,同一时间最大连接数)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
perror("listen error");
return false;
}
return true;
}
//客户端发送连接请求
bool Connect(const std::string& ip, const int port) {
//组织服务端的地址信息结构
sockaddr_in addr;
addr.sin_family = AF_INET;//IPV4地址域类型
addr.sin_port = htons(port);//端口从主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr(&ip[0]);//IP地址从主机字节序转换为网络字节序
socklen_t len = sizeof(sockaddr_in);
//connect(描述符,服务端地址, 地址长度)
int ret = connect(_sockfd, (sockaddr*)&addr, len);
if (ret < 0) {
perror("connect error");
return false;
}
return true;
}
//服务端新建连接
bool Accept(TcpSocket* sock, std::string* ip = NULL, uint16_t* port = NULL) {
//int accept(监听套接字, 获取客户端地址, 长度)
sockaddr_in addr;
socklen_t len = sizeof(sockaddr_in);
//新创建一个套接字来建立连接
int newfd = accept(_sockfd, (sockaddr*)&addr, &len);
if (newfd < 0) {
perror("accept error");
return false;
}
sock->_sockfd = newfd;
//建立连接时创建的套接字是包含五元组的,所以地址信息的接收可有可无
if (ip != NULL) {
*ip = inet_ntoa(addr.sin_addr);
}
if (port != NULL) {
*port = ntohs(addr.sin_port);
}
return true;
}
//接收数据
bool Recv(std::string* buf) {
//int recv(描述符,空间,数据长度,标志位)
//返回值:实际获取大小, 0-连接断开; -1-出错了
char tmp[4096] = { 0 };
//接收数据
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
perror("recv error");
return false;
}
//如果返回 0,则说明连接断开了
else if (ret == 0) {
printf("peer shutdown");
return false;
}
//使用临时空间接收数据,然后再传给接收区,这样可以保证接收区不会被创建的很大,而临时空间接着就会被销毁
buf->assign(tmp, ret);
return true;
}
//发送数据
bool Send(const std::string& data) {
//int send(描述符,数据,长度,标志位)
//TCP是字节流传输,所以可能数据没有一次性发送完毕,所以需要循环发送
//total记录了总共发送了多少字节数据
int total = 0;
//如果总共发送的数据比数据实际大小要小,则需要继续发送
while (total < data.size()) {
//发送数据
int ret = send(_sockfd, &data[0] + total, data.size() - total, 0);
if (ret < 0) {
perror("send error");
return false;
}
//累加总共发送了多少数据
total += ret;
}
return true;
}
//关闭套接字
bool Close() {
if (_sockfd != -1) {
close(_sockfd);
}
return true;
}
};
客户端程序
#include "tcpsocket.hpp"
int main(int argc, char *argv[]) {
//通过命令参数传入要连接的服务端的地址信息
if (argc != 3) {
printf("usage: ./tcp_cli srvip srvport\n");
return -1;
}
std::string srvip = argv[1]; //第一个命令参数为IP地址
uint16_t srvport = std::stoi(argv[2]); //第二个命令参数为端口号
TcpSocket cli_sock;
//1. 创建套接字
CHECK_RET(cli_sock.Socket());
//2. 绑定地址信息,客户端不推荐绑定地址信息
//3. 向服务端发起连接请求
CHECK_RET(cli_sock.Connect(srvip, srvport));
while(1) {
//4. 收发数据
std::string buf;
std::cout << "client say: ";
std::cin >> buf;
CHECK_RET(cli_sock.Send(buf));
buf.clear();
CHECK_RET(cli_sock.Recv(&buf));
std::cout << "server say: " << buf << std::endl;
}
//5. 关闭套接字
CHECK_RET(cli_sock.Close());
return 0;
}
服务端程序
- 首先我们知道 TCP 通信是需要建立连接的,也就是一对一进行通信,那么如果在服务端只用一个循环来建立多个连接,会有什么情况呢?
- 所以我们需要对于上面的情况做出处理,上面的问题就是一个执行流程不能完成多连接任务,所以可以考虑多执行流程序来解决该问题,而多执行流也有两种方法来实现:多线程、多进程;
- 多线程程序:多线程中主线程与子线程资源是共享的,所以在主线程中不需要关闭子线程的套接字,否则会出错,另外要么
thread_join
等待线程退出,要么将线程属性设置为detach
,这样可以避免资源泄露;
#include "tcpsocket.hpp"
#include <pthread.h>
//线程执行接口,在其中进行收发数据,参数传入的是套接字对象指针
void *thr_entry(void *arg) {
bool ret;
//获取套接字对象指针
TcpSocket *clisock = (TcpSocket*)arg;
//循环收发数据
while(1) {
//5. 收发数据--使用获取的新建套接字进行通信
std::string buf;
ret = clisock->Recv(&buf);
//接收数据失败的话,那么就关闭该套接字,并结束该线程(并不影响主线程的监听套接字)
if (ret == false) {
clisock->Close();
delete clisock;
return NULL;
}
//成功就把数据打印出来
std::cout << "client say: " << buf << std::endl;
buf.clear();
//然后进行数据回复
std::cout << "server say: ";
std::cin >> buf;
ret = clisock->Send(buf);
//如果回复数据失败也将套接字关闭,并结束该线程
if (ret == false) {
clisock->Close();
delete clisock;
return NULL;
}
}
//最终结束时关闭套接字
clisock->Close();
delete clisock;
return NULL;
}
int main(int argc, char *argv[]) {
//通过程序运行参数指定服务端要绑定的地址
// ./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字
//1. 创建套接字
CHECK_RET(lst_sock.Socket());
//2. 绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//3. 开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4. 获取新建连接,创建新的套接字
TcpSocket *clisock = new TcpSocket();
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
std::cout<<"get newconn:"<< cliip<<"-"<<cliport<<"\n";
//创建线程专门负责与指定客户端的通信
pthread_t tid;
pthread_create(&tid, NULL, thr_entry, (void*)clisock);
pthread_detach(tid);
}
//6. 关闭套接字
lst_sock.Close();
}
- 多进程程序:由于父进程与子进程之间资源独有,所以在创建新的套接字之后,父子进程中都回有一份,但是父进程中的那个套接字描述符我们用不到,所以需要将它关闭,另外我们还要关注子进程的退出,等待子进程退出或者显示忽略子进程的退出,可以避免子进程成为僵尸进程;
#include "tcpsocket.hpp"
#include <signal.h>
#include <sys/wait.h>
//信号处理函数,用来处理进程退出之后发给父进程的信号
void sigcb(int no){
while(waitpid(-1, NULL, WNOHANG) > 0);
}
//进程工作函数
void worker(TcpSocket &clisock){ //child process
bool ret;
while(1) {
//5. 收发数据--使用获取的新建套接字进行通信
std::string buf;
ret = clisock.Recv(&buf);
if (ret == false) {
clisock.Close();
exit(0);
}
std::cout <<"client say: "<<buf<<std::endl;
buf.clear();
std::cout << "server say: ";
std::cin >> buf;
ret = clisock.Send(buf);
if (ret == false) {
clisock.Close();
exit(0);
}
}
clisock.Close();//释放的是子进程的clisock
exit(0);
return;
}
int main(int argc, char *argv[]){
//通过程序运行参数指定服务端要绑定的地址
// ./tcp_srv 192.168.2.2 9000
if (argc != 3) {
printf("usage: ./tcp_src 192.168.2.2 9000\n");
return -1;
}
//以下为两种进程退出信号的处理方式
signal(SIGCHLD, SIG_IGN); //直接将退出信号置为显式忽略
signal(SIGCHLD, sigcb); //使用信号处理函数来等待进程退出
std::string srvip = argv[1];
uint16_t srvport = std::stoi(argv[2]);
TcpSocket lst_sock;//监听套接字
//1. 创建套接字
CHECK_RET(lst_sock.Socket());
//2. 绑定地址信息
CHECK_RET(lst_sock.Bind(srvip, srvport));
//3. 开始监听
CHECK_RET(lst_sock.Listen());
while(1) {
//4. 获取新建连接,创建新的套接字
TcpSocket clisock;
std::string cliip;
uint16_t cliport;
bool ret = lst_sock.Accept(&clisock, &cliip,&cliport);
if (ret == false) {
continue;
}
std::cout<<"get newconn:"<< cliip<<"-"<<cliport<<"\n";
pid_t pid = fork();
//如果进程创建失败,则关闭该套接字
if (pid < 0) {
clisock.Close();
continue;
}
//子进程中执行循环数据收发
else if (pid == 0) {
worker(clisock);
}
//父子进程数据独有,父进程关闭不会对子进程造成影响,不关闭反而会导致套接字描述符资源减少
clisock.Close();//释放的是父进程中的clisock
}
//6. 关闭套接字
lst_sock.Close();
}