目录

  • 基本知识
    • 通信两端
    • 查看网络信息
  • `sockaddr`结构
    • 概念
    • 介绍
  • 辅助接口介绍
    • 字节序转换接口
    • 地址转换接口
  • UDP协议
    • 概念
    • UDP通信流程
    • UDP通信接口
      • 创建套接字
      • 绑定地址信息
      • 接收数据
      • 发送数据
      • 关闭套接字
    • 实例
      • 使用C语言完成服务端功能
      • 使用C++对接口进行封装
  • TCP协议
    • 概念
    • TCP通信流程
    • TCP通信接口
      • 创建套接字
      • 绑定地址信息
      • 服务端监听
      • 客户端请求连接
      • 服务端新建连接
      • 接收数据
      • 发送数据
      • 关闭套接字
    • 实例
      • 封装TCP程序流程
      • 客户端程序
      • 服务端程序

基本知识

通信两端

  • 在网络通信程序中,通信两端被分为:

    • 客户端:通常是提供给客户的通信端,是编写通信程序中主动发起请求的一端;
    • 服务端:通常指被动接受请求,提供服务的通信端,在接受到客户端的请求之后,进行处理并返回;
  • 客户端是主动发送请求的一端,这也就意味着客户端必须提前知道服务端的地址信息(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_INETAF_INET6等;
    • scr:指向待转换的网络字节序二进制地址的指针;
    • dst:指向转换后的字符串地址的指针;
    • size:地址域类型所对应的地址信息结构体的大小;
  • int inet_pton(int af, const char* src, void* dst);:通用的转换接口,不局限于 IPv4,是将字符串地址转换为网络字节序二进制地址;
    • af:地址域类型;
    • src:指向待转换的字符串地址的指针;
    • dst:指向转换后的网络字节序二进制地址的指针;

UDP协议

概念

  • 概念:UDP 协议又叫用户数据报协议,是在传输层的协议;
  • 特性:无连接、不可靠传输、面向数据报;
  • 应用场景:实时性要求大于安全性的要求的场景;

UDP通信流程

  • 服务端流程

    1. 创建套接字,在内存中创建一个socket结构体;
    2. 为套接字绑定地址信息(组织地址结构体),在创建套接字时,在内存中所创建的socket结构体中加入 IP + port 信息,加入 port 信息目的是为了告诉操作系统,主机接收到的很多数据中,哪些数据应该交给当前的这个socket的接收缓冲区,加入 IP 地址信息是为了确定该套接字的源端地址信息;
    3. 接收数据,客户端向服务端发送数据后,服务端根据这个数据的地址信息来确定将数据放到哪个套接字的接收缓冲区,然后进程再从与其绑定的固定端口号的socket接收缓冲区中取出数据,一般来说,服务端的地址信息是固定的,这样客户端才能准确地将信息发送给服务端,并且服务端也能准确地接收数据;
    4. 发送数据,服务端在接收到数据后,根据数据中的信息确定要发往的对端地址,然后将要发送的数据放到socket的发送缓冲区中,内核选择合适的时候封装发送;
    5. 关闭套接字,套接字是内核中的结构体,占据一定资源关闭套接字会释放内核中占用的资源;
  • 客户端流程
    1. 创建套接字,在内存中创建一个socket结构体;
    2. 为套接字绑定地址,大部分情况下会忽略该步骤,因为当客户端将数据发送出去后,服务端就会拥有你的地址信息了,并且客户端的地址信息并不需要提供给谁,所以没有必要指定特定的地址信息;
    3. 发送数据,此时客户端已经确定了服务端的 IP + port,于是向固定的服务器发送数据,在发送时若socket没有绑定地址信息,则操作系统会选择合适的地址信息进行绑定;
    4. 接收数据;
    5. 关闭套接字;

UDP通信接口

创建套接字
  • int socket(int domain, int type, int protocol);

    • domain:地址域类型,AF_INET表示 IPV4,AF_INET6表示 IPV6,AF_UNIX表示本地通信;
    • typeSOCK_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);//如果不想获取数据发送方的地址信息,可以不传后面两个参数,默认为nullptrif (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通信流程

  • 服务端

    1. 创建套接字,这个套接字当做监听套接字使用;
    2. 为监听套接字绑定地址信息;
    3. 开始监听,将监听套接字置为 listen 状态,目的在于告诉系统可以处理客户端的连接请求了,从此刻开始,只要有一个新客户端连接请求,那么系统会为新客户端创建一个新的套接字,往后服务端与该客户端的通信就只靠这个套接字来完成;
    4. 获取新建连接,过程:利用监听套接字复制出新的套接字 s,然后为 s 绑定发送请求的客户端的地址信息,因为监听套接字原本就被绑定了服务端的地址信息了,所以 s 也拥有服务端的地址信息,这样一来,s 套接字就拥有了网络通信中的五元组(源端IP + 源端port + 对端IP + 对端port + TCP通信),此时就已完成建立连接了;
    5. 使用新的套接字收发数据;
    6. 一般关闭套接字的是新建的套接字,当服务端不在运行时,才会关闭监听套接字;
  • 客户端
    1. 创建套接字;
    2. 为套接字绑定地址信息,一般不绑定,等进行通信时系统会自动寻找合适的地址信息绑定;
    3. 向服务端发起连接请求,需要事先知道服务端的地址信息;
    4. 收发数据;
    5. 关闭套接字;

TCP通信接口

创建套接字
  • int socket(int domain, int type, int protocol);

    • domain:地址域类型,AF_INET表示 IPV4,AF_INET6表示 IPV6,AF_UNIX表示本地通信;
    • typeSOCK_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 9000if (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 9000if (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();
}

Linux网络——套接字编程相关推荐

  1. 【Linux】网络基础+UDP网络套接字编程

    只做自己喜欢做的事情,不被社会和时代裹挟着前进,是一件很奢侈的事. 文章目录 一. 网络基础 1.局域网和广域网 2.协议初识和网络协议分层(TCP/IP四层模型) 3.MAC地址和IP地址(子网掩码 ...

  2. 【Linux】网络套接字编程

    前言 在掌握一定的网络基础,我们便可以先从代码入手,利用UDP协议/TCP协议进行编写套接字程序,明白网络中服务器端与客户端之间如何进行连接并且通信的. 目录 一.了解源目的IP.端口.网络字节序.套 ...

  3. linux udp套接字编程获取报文源地址和源端口(二)

    之前项目中涉及udp套接字编程,其中一个要求是获取客户端发过来报文的端口和ip地址,功能很简单,只是对这一块不很熟.之前使用的方法是通过调用recvmsg这个接口,并通过参数msg里面的msg_nam ...

  4. socket网络套接字编程

    目录 UDP通信程序的编写: udp通信流程*: 接口认识: 字节序转换接口*: 查看网络连接状态的命令: 实现构建思路: udp_srv.c: udp_socket.hpp: udp_client. ...

  5. 网络套接字编程(socket 详解)

    socket 编程 套接字概念 Socket本身有"插座"的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型.本质为内核借助缓冲区形成的伪文件.既然是文件,那么理所当 ...

  6. 【网络篇】第五篇——网络套接字编程(一)(socket详解)

    socket编程 套接字概念 数据传输方式 ip地址转换函数 socket常见API sockaddr数据结构 socket缓冲区以及阻塞模式 LINUX下socket程序的演示 socket编程 套 ...

  7. 《c语言从入门到精通》看书笔记——第16章 网络套接字编程(下)——套接字

    1.套接字概述 套接字是网络通信的基石,是网络通信的基本构件. 所谓套接字,实际上是一个指向传输提供者的句柄.在WinSock中,就是通过操作该句柄来实现网络通信和管理的.根据性质和作用不同,套接字可 ...

  8. 《c语言从入门到精通》看书笔记——第16章 网络套接字编程(上)——网络

    1.IP地址 每台计算机都需要一个IP地址以识别自己,IP地址由IP协议规定的32位的二进制表示,最新的IPV6协议将IP地址提升为128位,但还不能广泛应用. 32位的IP地址主要分为前缀和后最两部 ...

  9. Linux下套接字详解(二)----套接字Socket

    在前面我们讲了TCP/IP.TCP和UDP的一些基本知识,但是协议只有一套,而我们系统多个TCP连接或多个应用程序进程必须通过同一个 TCP协议端口传输数据.为了区别不同的应用程序进程和连接,许多计算 ...

最新文章

  1. PyObject_CallMethod self问题
  2. 第7章 PCA与梯度上升法
  3. 关于 项目中Ioc基础模块 的搭建 (已适用于.net core / .net Framework / Nancy)
  4. 解决Sublime Text3莫名的中文乱码问题
  5. 前端学习(1183):指令v-cloak
  6. 高颜值的故宫介绍html源码
  7. getComputedStyle与currentStyle获取样式(style/class)
  8. webpack4.x实战七,生产模式和开发模式分开打包
  9. C#调用windows API实现 smallpdf客户端程序进行批量压缩
  10. 51单片机智能小车寻循迹代码
  11. 网络与系统安全笔记------身份认证技术
  12. 数学笔记25——弧长和曲面面积
  13. 服务器带宽打开网页很慢,网速快但是打开网页慢是怎么回事 浏览器打开网页慢的解决办法汇总...
  14. ELK系列之Mac安装kibana报错License information could not be obtained from Elasticsearch due to [invalid_inde
  15. 计算机硬件开关打开无线网络适配器,ibm笔记本电脑无线硬件开启步骤
  16. 视频格式转换器哪个好?用什么软件转换格式
  17. 小明酱的算法实习生面试准备
  18. 【Java基础】swing-图形界面学习(下)
  19. 华中数控系统CNC数据采集
  20. 逻辑思维只要5步读后感

热门文章

  1. 如何开发一个 WPS 加载项
  2. 20200524西瓜视频的视频下载打开的步骤(未完成)
  3. 在美国高校找教职及教学中的体会
  4. 微型计算机只要性能指标,微型计算机的主要性能指标运算速度.ppt
  5. 【小5聊】回看2022,展望2023,分享我的年度总结和感想,在一个行业十年,坚持下去你就是这个行业的专家
  6. 为什么不能每周发布一次?
  7. Unity3D中如何调用序列帧图片为动画
  8. 爬取B站弹幕制作词云图
  9. IDEA 安装与破解(亲测有效)
  10. Adobe PS 图片反转