【Linux】socket基础API

目录

1. 创建socket(TCP/UDP,客户端+服务器)

1.1 第一个参数——domain

1.2 第二个参数——type

1.3 第三个参数——protocol

2. 绑定socket地址(TCP/UDP,服务器)

2.1 字节序及转换函数

2.2 IP地址及转换函数

2.3 MAC地址

2.4 端口号

2.5 通用socket地址

2.6 专用socket地址

2.7 INADDR_ANY

2.8 为什么客户端不需要手动bind,服务器需要手动bind?

3. 监听socket(TCP,服务器)

4. 接受连接(TCP,服务器)

5. 发起连接(TCP,客户端)

6. 关闭连接(TCP/UDP,客户端+服务器)

7. 数据读写

7.1 TCP数据读写

7.2 UDP数据读写

8. 基于UDP的回声程序

9. 基于TCP的回声程序


1. 创建socket(TCP/UDP,客户端+服务器)

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 成功时返回socket文件描述符,失败时返回-1并设置errno
// domain      协议族
// type        socket类型
// protocol    协议

1.1 第一个参数——domain

协议族(protocol family,也称domain)是多个相关协议的集合。地址族类型通常与协议族类型对应。

协议族描述地址族描述
PF_INETlPv4协议族AF_INETlPv4地址族
PF_INET6lPv6协议族AF_INET6lPv6地址族

宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。

1.2 第二个参数——type

socket类型指的是socket的数据传输方式。

socket类型描述
SOCK_STREAM字节流式socket
SOCK_DGRAM数据报式socket

对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

传输层协议主要有两个:TCP协议和UDP协议。TCP协议相对于UDP协议的特点是:面向连接、字节流和可靠传输。

使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写。双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工的,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接以释放系统资源。

TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服务。而无连接协议UDP则非常适合于广播和多播。

字节流服务和数据报服务的区别对应到实际编程中,则体现为通信双方是否必须执行相同次数的读、写操作(当然,这只是表现形式)。当发送端应用程序连续执行多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此,TCP模块发送出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。

当接收端收到一个或多个TCP报文段后,TCP模块将它们携带的应用程序数据按照TCP报文段的序号依次放入TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收缓冲区中的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此,应用程序执行的读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。

综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节流的概念:应用程序对数据的发送和接收是没有边界限制的。UDP则不然。发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作(通过recvfrom系统调用),否则就会丢包(这经常发生在较慢的服务器上)。并且,如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。

TCP传输是可靠的。首先,TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。其次,TCP协议采用超时重传机制,发送端在发送出一个TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。最后,因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。

UDP协议则和IP协议一样,提供不可靠服务。它们都需要上层协议来处理数据确认和超时重传。

1.3 第三个参数——protocol

protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。

// 使用TCP协议
int tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
// 等价于 int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

// 使用UDP协议
int udp_socket = socket(PF_INET, SOCK_DGRAM, 0);
// 等价于 int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

2. 绑定socket地址(TCP/UDP,服务器)

创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。

“命名socket”,等价于“给socket绑定socket地址”,等价于“给socket分配IP地址和端口号”。

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
// 成功时返回0,失败时返回-1并设置errno
// sockfd     服务器socket文件描述符
// addr       指向服务器socket地址结构体
// addrlen    addr结构体变量的长度

2.1 字节序及转换函数

字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

字节序描述
大端字节序(Big Endian)低位字节存储在高地址处
小端字节序(Little Endian)低位字节存储在低地址处

如0x12345678,

大端模式:12 34 56 78

             低地址<--->高地址

小端模式:78 56 34 12

             低地址<--->高地址

为了防止数据在两台不同字节序的主机之间直接传递时解析错误,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order),统一为大端字节序。

字节序转换函数:

#include <arpa/inet.h>
// IP地址(32位)转换
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
// 端口号(16位)转换
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);
// h    host
// n    network
// l    long
// s    short

2.2 IP地址及转换函数

IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

IP地址描述
IPv4(Internet Protocol version 4)4字节地址族
IPv6(Internet Protocol version 6)16字节地址族

IPv4与IPv6的差别主要是表示IP地址所用的字节数,目前通用的地址族为IPv4。IPv6是为了应对2010年前后IP地址耗尽的问题而提出的标准,即便如此,现在还是主要使用IPv4,IPv6的普及将需要更长时间。

IPv4标准的4字节IP地址分为网络ID和主机ID,且分为A、B、C、D、E等类型。

同一个物理网络上的所有主机都使用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机ID与其对应。

E类IP地址不区分网络ID和主机ID,为将来使用保留。

只需通过IP地址的第一个字节即可判断网络地址占用的字节数,因为我们根据IP地址的边界区分网络地址,如下所示:

  • A类地址的首字节范围:0~127
  • B类地址的首字节范围:128~191
  • C类地址的首字节范围:192~223
  • D类地址的首字节范围:224~239
  • E类地址的首字节范围:240~255

还有如下这种表述方式:

  • A类地址的首位以0开始
  • B类地址的前2位以10开始
  • C类地址的前3位以110开始
  • D类地址的前4位以1110开始
  • E类地址的前5位以11110开始

正因如此,通过套接字收发数据时,数据传到网络后即可轻松找到正确的主机。

通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志时则相反,我们要把整数表示的IP地址转化为可读的字符串。下面3个函数可用于用点分十进制字符串表示的IPv4地址和用网络字节序整数表示的IPv4地址之间的转换:

#include <arpa/inet.h>

in_addr_t inet_addr(const char* cp);
// 将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址
// 成功时返回32位大端序整数型值,失败时返回INADDR_NONE

int inet_aton(const char* cp, struct in_addr* inp);
// 完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中
// 成功时返回1,失败时返回0

char* inet_ntoa(struct in_addr in);
// 将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址
// 成功时返回转换的字符串地址值,失败时返回-1

inet_ntoa函数调用时需小心,返回值类型为char指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息。总之,再次调用inet_ntoa函数前返回的字符串地址值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。

下面这对更新的函数也能完成和前面3个函数同样的功能,并且它们同时适用于IPv4地址和IPv6地址:

#include <arpa/inet.h>

int inet_pton(int af, const char* src, void* dst);
// 将用字符串表示的IP地址src(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)
// 转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中
// 成功时返回1,失败时返回0并设置errno
// af    地址族

const char* inet_ntop(int af, const void* src, char* dst, socklen_t size);
// 完成和inet_pton相反的功能
// 成功时返回目标存储单元的地址,失败时返回NULL并设置errno
// size    目标存储单元的大小

2.3 MAC地址

网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络适配器或网络接口卡NIC。其拥有MAC地址,属于OSI模型的第2层,它使得用户可以通过电缆或无线相互连接。每一个网卡都有一个被称为MAC地址的独一无二的48位串行号。网卡的主要功能:1. 数据的封装与解封装;2. 链路管理;3. 数据编码与译码。

MAC地址(Media Access Control Address),直译为媒体存取控制位址,也称为局域网地址、以太网地址、物理地址或硬件地址,它是一个用来确认网络设备位置的位址,由网络设备制造商生产时烧录在网卡中。在OSI模型中,第三层网络层负责IP地址,第二层数据链路层则负责MAC地址。MAC地址用于在网络中唯一标识一个网卡,一台设备若有一或多个网卡,则每个网卡都需要并会有一个唯一的MAC地址。

MAC地址的长度为48 位(6个字节),通常表示为12个16进制数,如:00-16-EA-AE-3C-40,就是一个MAC地址,其中前3个字节,16进制数00-16-EA代表网络硬件制造商的编号,它由IEEE(电气与电子工程师协会)分配,而后3个字节,16进制数AE-3C-40代表该制造商所制造的某个网络产品(如网卡)的系列号。只要不更改自己的MAC地址,MAC地址在世界是唯一的。形象地说,MAC地址就如同身份证上的身份证号码,具有唯一性。

2.4 端口号

端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将1个端口号分配给不同套接字。另外,端口号由16位构成,可分配的端口号范围是0~65535。但0~1023是知名端口(Well-known PORT),一般分配给特定应用程序,所以应当分配此范围之外的值。另外,虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。例如:如果某TCP套接字使用9190号端口,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用。

总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的目的应用程序(应用程序套接字)。

2.5 通用socket地址

struct sockaddr
{
	sa_family_t    sin_family;  // 地址族
	char		   sa_data[14]; // 地址信息
};

此结构体成员sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应填充0,这也是bind函数要求的。而这对于包含地址信息来讲非常麻烦,继而就有了新的结构体sockaddr_in。

2.6 专用socket地址

表示IPv4地址的结构体:

struct sockaddr_in
{
	sa_family_t		  sin_family;  // 地址族
	in_port_t		  sin_port;    // 16位端口号,以网络字节序保存
	struct in_addr    sin_addr;    // 32位IP地址,以网络字节序保存
	char			  sin_zero[8]; // 不使用
};

其中,stuct in_addr定义如下:

struct in_addr
{
	in_addr_t    s_addr; // 32位IPv4地址
};

sin_zero[8]无特殊含义。只是为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员。必需填充为0,否则无法得到想要的结果。

所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。

2.7 INADDR_ANY

每次创建服务器端socket都要输入IP地址会有些繁琐,此时可如下初始化地址信息。

addr.sin_addr.s_addr = INADDR_ANY;

若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。而且,若同一计算机中已分配多个IP地址(多宿主(Multi-homed)计算机,一般路由器属于这一类),则只要端口号一致,就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能,否则不会采用。

2.8 为什么客户端不需要手动bind,服务器需要手动bind?

客户端socket也需要绑定socket地址,但是不需要手动绑定,是操作系统自动绑定的。客户端的端口号是操作系统随机分配的,防止客户端出现启动冲突。

服务器为什么需要手动bind?

  • 服务器的端口号是众所周知且不能随意改变的
  • 同一家公司的端口号需要统一规范化

3. 监听socket(TCP,服务器)

socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列(连接请求队列)以存放待处理的客户连接:

#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 成功时返回0,失败时返回-1并设置errno
// sockfd     服务器socket文件描述符
// backlog    监听队列的最大长度,一般为5

4. 接受连接(TCP,服务器)

下面的系统调用从listen监听队列中接受一个连接:

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
// 成功时返回文件描述符,失败时返回-1并设置errno
// sockfd     服务器socket文件描述符
// addr       输出型参数,指向客户端socket地址结构体
// addrlen    输出型参数,指向addr结构体变量的长度

accept函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。需要强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。下图展示了accept函数调用过程。

5. 发起连接(TCP,客户端)

如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
// 成功时返回0,失败时返回-1并设置errno
// sockfd      客户端socket文件描述符
// addr        指向服务器socket地址结构体
// addrlen     addr结构体变量的长度

6. 关闭连接(TCP/UDP,客户端+服务器)

关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:

#include <unistd.h>
int close(int sockfd);
// 成功时返回0,失败时返回-1并设置errno

close系统调用并非总是立即关闭一个连接,而是将sockfd的引用计数减1。只有当sockfd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以使用如下的shutdown系统调用(相对于close来说,它是专门为网络编程设计的):

#include <sys/socket.h>
int shutdown(int socket, int how);
// 成功时返回0,失败时返回-1并设置errno
// how    断开连接的方式:SHUT_RD SHUT_WR SHUT_RDWR

shutdown能够分别关闭socket上的读或写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。

7. 数据读写

7.1 TCP数据读写

对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void* buf, size_t len, int flags);
// 成功时返回实际写入的数据的长度,失败时返回-1并设置errno
// sockfd    发送端socket文件描述符

ssize_t recv(int sockfd, void* buf, size_t len, int flags);
// 成功时返回实际读取的数据的长度,失败时返回-1并设置errno
// sockfd    接收端socket文件描述符

// buf      指向缓冲区
// len      缓冲区的长度
// flags    为数据收发提供了额外的控制,通常设置为0

7.2 UDP数据读写

socket编程接口中用于UDP数据报读写的系统调用是:

#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void* buf, size_t len, int flags,
               const struct sockaddr* dest_addr, socklen_t addrlen);
// 成功时返回实际写入的数据的长度,失败时返回-1并设置errno
// sockfd       发送端socket文件描述符
// dest_addr    指向接收端socket地址结构体
// addrlen      dest_addr结构体变量的长度

ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,
                 struct sockaddr* src_addr, socklen_t* addrlen);
// 成功时返回实际读取的数据的长度,失败时返回-1并设置errno
// sockfd      接收端socket文件描述符
// src_addr    输出型参数,指向发送端socket地址结构体
// addrlen     输出型参数,指向src_addr结构体变量的长度,要用sizeof(src_addr)初始化

// buf      指向缓冲区
// len      缓冲区的长度
// flags    为数据收发提供了额外的控制,通常设置为0

8. 基于UDP的回声程序

我们可以把socket封装起来,也可以不封装。

这里我们展示把服务器的socket封装,客户端的socket就不封装了。

udp_server.hpp:

#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>

namespace udp_server
{
    using namespace std;

    class UdpServer
    {
    public:
        UdpServer(in_port_t port = 8080)
            : _port(port)
        {
            cout << "port: " << _port << endl;
        }

        void InitServer()
        {
            // 1. 创建socket

            // 创建UDP socket
            _sockfd = socket(PF_INET, SOCK_DGRAM, 0);
            // 如果创建socket失败
            if (_sockfd < 0)
            {
                perror("socket() failed");
                exit(1);
            }
            // 创建socket成功
            cout << "socket() succeeded. sockfd is " << _sockfd << endl;

            // 2. 绑定socket地址

            // 设置IPv4专用socket地址:sockaddr_in
            struct sockaddr_in addr;
            bzero(&addr, sizeof(addr)); // 等价于memset(&addr, 0, sizeof(addr));
            addr.sin_family = AF_INET;         // 设置地址族
            addr.sin_port = htons(_port);      // 设置端口号,要转换成网络字节序
            addr.sin_addr.s_addr = INADDR_ANY; // 设置IP地址
            // 绑定socket地址
            int ret = bind(_sockfd, (struct sockaddr*)&addr, sizeof(addr));
            // 如果绑定socket地址失败
            if (ret < 0)
            {
                perror("bind() failed");
                exit(2);
            }
            // 绑定socket地址成功
            cout << "bind() succeeded. sockfd is " << _sockfd << endl;
        }

        void Start()
        {
            // 循环收发数据
            char buf[1024];
            while (1)
            {
                // 接收数据

                // 从客户端接收数据放到buf中
                struct sockaddr_in client_addr;
                socklen_t client_addrlen = sizeof(client_addr);
                int n = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&client_addr, &client_addrlen);
                if (n > 0)
                {
                    buf[n] = '\0';
                }
                else
                    break;
                // 提取客户端信息
                string client_ip = inet_ntoa(client_addr.sin_addr);  // 网络字节序整数->点分十进制字符串
                in_port_t client_port = ntohs(client_addr.sin_port); // 网络字节序->主机字节序
                // 打印收到的数据
                cout << client_ip << " " << client_port << ": " << buf << endl;

                // 发送数据

                // 将从客户端接收到的数据再转发给客户端
                sendto(_sockfd, buf, strlen(buf), 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
            }
        }
        
        ~UdpServer() {}

    private:
        int _sockfd;
        in_port_t _port;
    };
}

udp_server.cc:

#include "udp_server.hpp"
#include <memory>

using namespace udp_server;

int main(int argc, char* argv[])
{
    // 使用说明:./udp_server 服务器端口号
    if (argc != 2)
    {
        cout << "Usage:\n\t" << argv[0] << " <server_port>\n" << endl; 
    }

    in_port_t server_port = atoi(argv[1]);

    unique_ptr<UdpServer> userv(new UdpServer(server_port));

    userv->InitServer();
    userv->Start();

    return 0;
}

udp_client.cc:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>

using namespace std;

int main(int argc, char *argv[])
{
    // 使用说明:./udp_clinet 服务器IP 服务器端口号
    if (argc != 3)
    {
        cout << "Usage:\n\t" << argv[0] << " <server_ip>" << " <server_port>\n" << endl;
    }

    string server_ip = argv[1];
    in_port_t server_port = atoi(argv[2]);

    // 1. 创建socket

    // 创建UDP socket
    int sockfd = socket(PF_INET, SOCK_DGRAM, 0);
    // 如果创建socket失败
    if (sockfd < 0)
    {
        perror("socket() failed");
        exit(1);
    }
    // 创建socket成功
    cout << "socket() succeeded. sockfd is " << sockfd << endl;

    // 明确服务器是谁
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr)); // 等价于memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;                           // 设置地址族
    server_addr.sin_port = htons(server_port);                  // 设置端口号,要转换成网络字节序
    server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 设置IP地址

    // 循环发收数据
    while (1)
    {
        // 发送数据

        // 用户输入数据
        string message;
        cout << "please enter: ";
        cin >> message;
        // 给服务器发送数据
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));

        // 接收数据

        // 从服务器接收数据放到buf中
        char buf[1024];
        struct sockaddr_in tmp_addr;
        socklen_t tmp_addrlen = sizeof(tmp_addr);
        int n = recvfrom(sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr*)&tmp_addr, &tmp_addrlen);
        if (n > 0)
        {
            buf[n] = '\0';
        }
        else
            break;
        // 打印收到的数据
        cout << "echo: " << buf << endl;
    }

    return 0;
}

Makefile:

.PHONY:all
all: udp_client udp_server

udp_client:udp_client.cc
	g++ $^ -o $@ -std=c++11
udp_server:udp_server.cc
	g++ $^ -o $@ -std=c++11

.PHONY:clean
clean:
	rm -f udp_client udp_server

9. 基于TCP的回声程序

这里我们展示把服务器的socket封装,客户端的socket就不封装了。

tcp_server.hpp:

#pragma once

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>

namespace tcp_server
{
    using namespace std;

    class TcpServer
    {
    public:
        TcpServer(in_port_t port = 8080)
            : _port(port)
        {
            cout << "port: " << _port << endl;
        }

        void InitServer()
        {
            // 1. 创建socket

            // 创建TCP socket
            _sockfd = socket(PF_INET, SOCK_STREAM, 0);
            // 如果创建socket失败
            if (_sockfd < 0)
            {
                perror("socket() failed");
                exit(1);
            }
            // 创建socket成功
            cout << "socket() succeeded. sockfd is " << _sockfd << endl;

            // 2. 绑定socket地址

            // 设置IPv4专用socket地址:sockaddr_in
            struct sockaddr_in addr;
            bzero(&addr, sizeof(addr)); // 等价于memset(&addr, 0, sizeof(addr));
            addr.sin_family = AF_INET;         // 设置地址族
            addr.sin_port = htons(_port);      // 设置端口号,要转换成网络字节序
            addr.sin_addr.s_addr = INADDR_ANY; // 设置IP地址
            // 绑定socket地址
            int ret = bind(_sockfd, (struct sockaddr*)&addr, sizeof(addr));
            // 如果绑定socket地址失败
            if (ret < 0)
            {
                perror("bind() failed");
                exit(2);
            }
            // 绑定socket地址成功
            cout << "bind() succeeded. sockfd is " << _sockfd << endl;

            // 3. 监听socket

            // 监听socket
            ret = listen(_sockfd, 5);
            // 如果监听socket失败
            if (ret < 0)
            {
                perror("listen() failed");
                exit(3);
            }
            // 监听socket成功
            cout << "listen() succeeded. sockfd is " << _sockfd << endl;
        }

        void Start()
        {
            while (1)
            {   
                // 4. 接受连接

                // 创建客户端socket地址,作为输出型参数
                struct sockaddr_in client_addr;
                socklen_t client_addrlen = sizeof(client_addr);
                // 接受连接
                int accept_sockfd = accept(_sockfd, (struct sockaddr*)&client_addr, &client_addrlen);
                // 如果接受连接失败
                if (accept_sockfd < 0)
                {
                    perror("accept() failed");
                    exit(4);
                }
                // 接受连接成功
                cout << "accept() succeeded. accept_sockfd is " << _sockfd << endl;
                // 提取客户端信息
                string client_ip = inet_ntoa(client_addr.sin_addr);  // 网络字节序整数->点分十进制字符串
                in_port_t client_port = ntohs(client_addr.sin_port); // 网络字节序->主机字节序

                // 循环收发数据
                char buf[1024];
                while (1)
                {
                    // 接收数据

                    // 从客户端接收数据放到buf中
                    int n = recv(accept_sockfd, buf, sizeof(buf) - 1, 0);
                    if (n > 0) // 接收成功
                    {
                        buf[n] = '\0';
                        // 打印收到的数据
                        cout << client_ip << " " << client_port << ": " << buf << endl;
                    }
                    else if (n == 0) // 客户端将连接关闭了
                    {
                        close(accept_sockfd);
                        cout << "client quit" << endl;
                        break;
                    }
                    else // 接收失败
                    {
                        close(accept_sockfd);
                        perror("recv() failed");
                        break;
                    }

                    // 发送数据

                    // 将从客户端接收到的数据再转发给客户端
                    send(accept_sockfd, buf, strlen(buf), 0);
                }
            }
        }
        
        ~TcpServer() {}

    private:
        int _sockfd;
        in_port_t _port;
    };
}

tcp_server.cc:

#include "tcp_server.hpp"
#include <memory>

using namespace tcp_server;

int main(int argc, char* argv[])
{
    // 使用说明:./tcp_server 服务器端口号
    if (argc != 2)
    {
        cout << "Usage:\n\t" << argv[0] << " <server_port>\n" << endl; 
    }

    in_port_t server_port = atoi(argv[1]);

    unique_ptr<TcpServer> tserv(new TcpServer(server_port));

    tserv->InitServer();
    tserv->Start();

    return 0;
}

tcp_client.cc:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>

using namespace std;

int main(int argc, char *argv[])
{
    // 使用说明:./tcp_clinet 服务器IP 服务器端口号
    if (argc != 3)
    {
        cout << "Usage:\n\t" << argv[0] << " <server_ip>" << " <server_port>\n" << endl;
    }

    string server_ip = argv[1];
    in_port_t server_port = atoi(argv[2]);

    // 1. 创建socket

    // 创建TCP socket
    int sockfd = socket(PF_INET, SOCK_STREAM, 0);
    // 如果创建socket失败
    if (sockfd < 0)
    {
        perror("socket() failed");
        exit(1);
    }
    // 创建socket成功
    cout << "socket() succeeded. sockfd is " << sockfd << endl;

    // 明确服务器是谁
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr)); // 等价于memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;                           // 设置地址族
    server_addr.sin_port = htons(server_port);                  // 设置端口号,要转换成网络字节序
    server_addr.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 设置IP地址

    // 2. 发起连接
    int count = 5;
    while (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)))
    {
        sleep(1);
        cout << "正在尝试重连,重连次数还有: " << count-- << endl;
        if (count <= 0)
            break;
    }
    // 如果发起连接失败
    if (count <= 0)
    {
        perror("connect() failed");
        exit(5);
    }
    // 发起连接成功
    cout << "connect() succeeded. sockfd is " << sockfd << endl;

    // 循环发收数据
    while (1)
    {
        // 发送数据

        // 用户输入数据
        string message;
        cout << "please enter: ";
        cin >> message;
        // 给服务器发送数据
        send(sockfd, message.c_str(), message.size(), 0);

        // 2. 接收数据

        // 从服务器接收数据放到buf中
        char buf[1024];
        int n = recv(sockfd, buf, sizeof(buf) - 1, 0);
        if (n > 0) // 接收成功
        {
            buf[n] = '\0';
            // 打印收到的数据
            cout << "echo: " << buf << endl;
        }
        else if (n == 0) // 服务器将连接关闭了
        {
            close(sockfd);
            cout << "server quit" << endl;
            break;
        }
        else // 接收失败
        {
            close(sockfd);
            perror("recv() failed");
            break;
        }
    }

    return 0;
}

Makefile:

.PHONY:all
all: tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ $^ -o $@ -std=c++11
tcp_server:tcp_server.cc
	g++ $^ -o $@ -std=c++11

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/285043.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

听GPT 讲Rust源代码--library/proc_macro

File: rust/library/proc_macro/src/bridge/rpc.rs 在Rust源代码中&#xff0c;rust/library/proc_macro/src/bridge/rpc.rs文件的作用是实现了Rust编程语言的编译过程中的远程过程调用&#xff08;RPC&#xff09;机制。 这个文件定义了与编译器的交互过程中使用的各种数据结构…

bilibili深入理解计算机系统笔记(3):使用C语言实现静态链接器

本文是2022年的项目笔记&#xff0c;2024年1月1日整理文件的时候发现之&#xff0c;还是决定发布出来。 Github链接&#xff1a;https://github.com/shizhengLi/csapp_bilibili 文章目录 可执行链接文件(ELF)ELF headerSection header符号表symtab二进制数如何和symtab结构成员…

OpenCV-Python(29):图像特征

目录 目标 背景介绍 常用特征 应用场景 目标 理解什么是图像特征 为什么图像特征很重要 为什么角点很重要 背景介绍 相信大多数人都玩过拼图游戏吧。首先你们拿到一张图片的一堆碎片&#xff0c;你要做的就是把这些碎片以正确的方式排列起来从而重建这幅图像。问题是&…

【并发设计模式】聊聊Thread-Per-Message与Worker-Thread模式

在并发编程中&#xff0c;核心就是同步、互斥、分工。 同步是多个线程之间按照一定的顺序进行执行&#xff0c;比如A执行完&#xff0c;B在执行。而互斥是多个线程之间对于共享资源的互斥。两个侧重点不一样&#xff0c;同步关注的是执行顺序&#xff0c;互斥关注的是资源的排…

RedisTemplate序列化

SpringBoot整合Redis&#xff0c;配置RedisTemplate序列化。如果使用StringRedisTemplate&#xff0c;那么不需要配置序列化&#xff0c;但是StringRedisTemplate只能存储简单的String类型数据&#xff0c;如图&#xff1a; 如果使用StringRedisTemplate存储一个常规对象&#…

python实现Ethernet/IP协议的客户端(三)

Ethernet/IP是一种工业自动化领域中常用的网络通信协议&#xff0c;它是基于标准以太网技术的应用层协议。作为工业领域的通信协议之一&#xff0c;Ethernet/IP 提供了一种在工业自动化设备之间实现通信和数据交换的标准化方法。python要实现Ethernet/IP的客户端&#xff0c;可…

灸哥问答:软件架构在软件研发中的作用

软件架构在软件开发中扮演着至关重要的角色。我们在软件研发的过程中&#xff0c;类比于建造一座公寓楼&#xff0c;而软件架构就像是盖楼之前的设计图纸&#xff0c;如果没有设计图纸就直接盖楼&#xff0c;可想而知带来的后果是什么。我对软件架构的作用表现总结如下&#xf…

Go语言TCP Socket编程

:::tip 声明 本文源于Go语言TCP Socket编程 | Tony Bai&#xff0c;可能会有稍微的修改。 ::: 文章目录 一、模型二、TCP连接的建立对于客户端而言&#xff0c;连接的建立会遇到如下几种情形&#xff1a;1、网络不可达或对方服务未启动2、对方服务的listen backlog满3、网络延…

【重磅新品】小眼睛科技推出紫光同创盘古系列FPGA开发板套件,盘古200K开发板,紫光同创PG2L200H,Logos2系列

FPGA&#xff0c;即现场可编程门阵列&#xff0c;作为可重构电路芯片&#xff0c;已经成为行业“万能芯片”&#xff0c;在通信系统、数字信息处理、视频图像处理、高速接口设计等方面都有不俗的表现。近几年&#xff0c;随着国家战略支持和产业发展&#xff0c;国产FPGA迎来迅…

PyTorch官网demo解读——第一个神经网络(4)

上一篇&#xff1a;PyTorch官网demo解读——第一个神经网络&#xff08;3&#xff09;-CSDN博客 上一篇我们聊了手写数字识别神经网络的损失函数和梯度下降算法&#xff0c;这一篇我们来聊聊激活函数。 大佬说激活函数的作用是让神经网络产生非线性&#xff0c;类似人脑神经元…

Python算法例33 删除数字

1. 问题描述 给出一个字符串A&#xff0c;表示一个n位的正整数&#xff0c;删除其中k位数字&#xff0c;使得剩余的数字仍然按照原来的顺序排列产生一个新的正整数&#xff0c;本例将找到删除k个数字之后的最小正整数&#xff0c;其中n≤240&#xff0c;k≤n。 2. 问题示例 …

HarmonyOS4.0系统性深入开发10卡片事件能力说明

卡片事件能力说明 ArkTS卡片中提供了postCardAction()接口用于卡片内部和提供方应用间的交互&#xff0c;当前支持router、message和call三种类型的事件&#xff0c;仅在卡片中可以调用。 接口定义&#xff1a;postCardAction(component: Object, action: Object): void 接口…

性能优化(CPU优化技术)-ARM Neon详细介绍

本文主要介绍ARM Neon技术&#xff0c;包括SIMD技术、SIMT、ARM Neon的指令、寄存器、意图为读者提供对ARM Neon的一个整体理解。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;高性能&#xff08;HPC&#xff09…

IRQ Handler 的使用——以USART串口接收中断分别在标准库与HAL库版本下的举例

前言&#xff1a; 1.中断系统及EXTI外部中断知识点见我的博文&#xff1a; 9.中断系统、EXTI外部中断_eirq-CSDN博客文章浏览阅读301次&#xff0c;点赞7次&#xff0c;收藏6次。EXTI&#xff08;Extern Interrupt&#xff09;外部中断EXTI可以监测指定GPIO口的电平信号&…

关于镜头景深的计算

1、问题背景 在调试项目的过程中&#xff0c;我们需要知道所搭配镜头的对焦距离、景深范围是多少&#xff0c; 这属于基本的项目信息&#xff0c;很多时候往往就因为忽略了这些小的信息&#xff0c;而导致一系列问题。 比如之前调试的一款化妆镜的设备&#xff0c;客户反馈了…

bootstrap5开发房地产代理公司Hamilton前端页面

一、需求分析 房地产代理网站是指专门为房地产行业提供服务的在线平台。这些网站的主要功能是连接房地产中介机构、房产开发商和潜在的买家或租户&#xff0c;以促成买卖或租赁房产的交易。以下是一些常见的房地产代理网站的功能&#xff1a; 房源发布&#xff1a;房地产代理网…

git解决冲突场景

文章目录 git解决冲突场景 git解决冲突场景 假设我们在公司开发了一个功能修改了一个文件 我们现在模拟修改文件之后提交一个版本到本地&#xff0c;但是不上传到远程仓库 假设我们现在回到家开发代码&#xff0c;需要拉去最新的代码 提示已经更新。根本没有最新的代码改动&am…

Android Studio 如何申请免费的api接口之聚合数据--建议收藏备用!

目录 前言 一、申请接口 二、使用接口 三、总结 四、更多资源 前言 在开发应用程序过程中&#xff0c;获取免费的 API 接口和数据源是非常重要的。它们可以为你的应用程序提供各种功能和数据&#xff0c;使其更加实用和丰富。本文将介绍如何申请免费的 API 接口以及一些建…

【图像拼接】源码精读:Seam-guided local alignment and stitching for large parallax images

第一次来请先看这篇文章&#xff1a;【图像拼接&#xff08;Image Stitching&#xff09;】关于【图像拼接论文源码精读】专栏的相关说明&#xff0c;包含专栏内文章结构说明、源码阅读顺序、培养代码能力、如何创新等&#xff08;不定期更新&#xff09; 【图像拼接论文源码精…

Vue3全局属性app.config.globalProperties

文章目录 一、概念二、实践2.1、定义2.2、使用 三、最后 一、概念 一个用于注册能够被应用内所有组件实例访问到的全局属性的对象。点击【前往】访问官网 二、实践 2.1、定义 在main.ts文件中设置app.config.globalPropertie import {createApp} from vue import ElementPl…