【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_NONEint inet_aton(const char* cp, struct in_addr* inp);
// 完成和inet_addr同样的功能,但是将转化结果存储于参数inp指向的地址结构中
// 成功时返回1,失败时返回0char* 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_instruct 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';}elsebreak;// 提取客户端信息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 socketint 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';}elsebreak;// 打印收到的数据cout << "echo: " << buf << endl;}return 0;
}

Makefile:

.PHONY:all
all: udp_client udp_serverudp_client:udp_client.ccg++ $^ -o $@ -std=c++11
udp_server:udp_server.ccg++ $^ -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_instruct 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// 监听socketret = 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 socketint 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_servertcp_client:tcp_client.ccg++ $^ -o $@ -std=c++11
tcp_server:tcp_server.ccg++ $^ -o $@ -std=c++11.PHONY:clean
clean:rm -f tcp_client tcp_server

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

消融实验(ablation study)——全网最全解读

消融实验&#xff08;ablation study&#xff09; 是什么优势与劣势案例总结 是什么 消融实验是一种科学研究方法&#xff0c;用于确定一个条件或参数对结果的影响程度。当研究者提出了一个新的方案或方法时&#xff0c;消融实验通过逐一控制一个条件或参数&#xff0c;来观察…

6个火爆全网的AI开源项目,用上月10万+

标题月10万可能说的有点夸张和含糊&#xff0c;10万具体指的是你可以利用这些开源项目实现&#xff1a; 访问量10万 收入10万 用户10万 …… 开源项目只是免费的工具&#xff0c;具体怎么实现还需要你根据自己需求去深入运营。这里只是给你推荐一些比较热门的开源项目&…

基于QT开发的温室气体数据记录软件

1、概述 温室气体分析仪数据记录软件用于实现温室气体分析仪数据的获取与存储&#xff0c;阀箱数据的获取与存储、冷阱数据的获取与存储、采样单元数据的获取与存储、阀箱和采样单元的远程操作以及系统功能的管理。其主操作界面如下&#xff1a; 上述软件界面分为2各区域&…

用html,js和layui写一个简单的点击打怪小游戏

介绍&#xff1a; 一个简单的打怪小游戏&#xff0c;点击开始游戏后&#xff0c;出现攻击按钮&#xff0c;击败怪物后可以选择继续下一关和结束游戏。 继续下一个怪兽的血量会增加5点&#xff0c;攻击按钮会随机变色。 效果图&#xff1a; html代码&#xff1a; <!DOCTYPE…

QDialog

属性方法 样式表 background-color: qlineargradient(spread:reflect, x1:0.999896, y1:0.494136, x2:1, y2:1, stop:0 rgba(0, 0, 0, 255), stop:1 rgba(255, 255, 255, 255));border: 1px groove rgb(232, 232, 232);border-radius: 20px; QDialog 的常用方法&#xff1a; e…

前端 js 基础(1)

js 结果输出 &#xff08;点击按钮修改文字 &#xff09; <!DOCTYPE html> <html> <head></head><body><h2>Head 中的 JavaScript</h2><p id"demo">一个段落。</p><button type"button" onclic…

基于PHP的校园代购商城系统

有需要请加文章底部Q哦 可远程调试 基于PHP的校园代购商城系统 一 介绍 此校园代购商城系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。(附带参考设计文档) 技术栈&#xff1a;phpmysqlbootstrapphpstudyvscode 二 功能 …

2023/12/30 c++ work

定义一个Person类&#xff0c;私有成员int age&#xff0c;string &name&#xff0c;定义一个Stu类&#xff0c;包含私有成员double *score&#xff0c;写出两个类的构造函数、析构函数、拷贝构造和拷贝赋值函数&#xff0c;完成对Person的运算符重载(算术运算符、条件运算…

03 团队研究进一步详细介绍

一、印第安纳大学邢璐祎课题组 【团队网站】 https://www.xing-luyi.com/ 【团队介绍】 研究以形式化方法为特色&#xff0c;并保证系统中的安全性和隐私合规性&#xff0c;特别是物联网、云、移动和软件供应链。 【团队成果汇总】 物联网系统&#xff1a;[Oakland24][Se…

骑砍战团MOD开发(30)-游戏大地图map.txt

骑砍1战团mod开发-大地图制作方法_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1rz4y1c7wH/ 一.骑砍游戏大地图 骑砍RTS视角游戏大地图 大地图静态模型(map.txt) 军团/城镇图标(module_parties.py). 骑砍大地图的战争迷雾和天气通过API进行管理和控制: # Weather-h…

分库分表之Mycat应用学习五

5 Mycat 离线扩缩容 当我们规划了数据分片&#xff0c;而数据已经超过了单个节点的存储上线&#xff0c;或者需要下线节 点的时候&#xff0c;就需要对数据重新分片。 5.1 Mycat 自带的工具 5.1.1 准备工作 1、mycat 所在环境安装 mysql 客户端程序。 2、mycat 的 lib 目录…

48、激活函数 - 梯度消失和梯度爆炸

简单介绍下梯度消失和梯度爆炸,这个不是重点,但是我觉得有必要再深入了解这个概念,以及很多激活函数为什么是可以防止梯度消失的。 梯度消失和梯度爆炸实际上是在神经网络训练过程中经常会遇到的两类问题,这两类问题都与梯度有关。 什么是梯度 在神经网络训练中,梯度是指…

深度学习 | Transformer模型及代码实现

Transformer 是 Google 的团队在 2017 年提出的一种 NLP 经典模型&#xff0c;现在比较火热的 Bert 也是基于 Transformer。Transformer 模型使用了 Self-Attention 机制&#xff0c;不采用 RNN 的顺序结构&#xff0c;使得模型可以并行化训练&#xff0c;而且能够拥有全局信息…

一年百模大战下来,有哪些技术趋势和行业真相逐渐浮出水面?

介绍 本人是独立开源软件开发者&#xff0c;参与很多项目建设&#xff0c;谈下感受。 ChatGPT开始AI生成元年&#xff0c;经历一年依然是第一。 LLaMA的巧合开启开源大模型浪潮。 名词解释 AIGC : AI-Generated Content 指利用人工智能技术&#xff08;生成式AI路径&#x…

类和接口

内容大部分来源于学习笔记&#xff0c;随手记录笔记内容以及个人笔记 对象Object java是面向对象的语言&#xff0c;一个对象包含状态和行为 可以这样理解&#xff0c;我眼前的石头&#xff0c;手里水杯&#xff0c;这些具体到某一个个体&#xff0c;这就是对象&#xff1b;…

非科班,培训出身,怎么进大厂?

今天分享一下我是怎么进大厂的经历&#xff0c;希望能给大家带来一点点启发&#xff01; 阿七毕业于上海一所大学的管理学院&#xff0c;在读期间没写过一行 Java 代码。毕业之后二战考研失利。 回过头来看&#xff0c;也很庆幸这次考研失利&#xff0c;因为这个时候对社会一…

OpenOCD简介和下载安装(Ubuntu)

文章目录 OpenOCD简介OpenOCD软件模块OpenOCD源码下载OpenOCD安装 OpenOCD简介 OpenOCD&#xff08;Open On-Chip Debugger&#xff09;开放式片上调试器 OpenOCD官网 https://openocd.org/&#xff0c;进入官网点击 About 可以看到OpenOCD最初的设计是由国外一个叫Dominic Ra…

红队打靶练习:SAR: 1

目录 信息收集 1、arp 2、netdiscover 3、nmap 4、nikto 5、whatweb 小结 目录探测 1、gobuster 2、dirsearch WEB CMS 1、cms漏洞探索 2、RCE漏洞利用 提权 get user.txt 本地提权 信息收集 1、arp ┌──(root㉿ru)-[~/kali] └─# arp-scan -l Interface:…

写在2024年初,软件测试面试笔记总结与分享

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;得准备年后面试了&#xff0c;又不知道从何下手&#xff01;为了帮大家节约时间&#xff0c;特意准备了一份面试相关的资料&#xff0c;内容非常的全面&#xff0c;真的可以好好补一补&#xff0c;希望大家在都能拿到…

LanceDB:在对抗数据复杂性战役中,您可信赖的坐骑

LanceDB 建立在 Lance&#xff08;一种开源列式数据格式&#xff09;之上&#xff0c;具有一些有趣的功能&#xff0c;使其对 AI/ML 具有吸引力。例如&#xff0c;LanceDB 支持显式和隐式矢量化&#xff0c;能够处理各种数据类型。LanceDB 与 PyTorch 和 TensorFlow 等领先的 M…