【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.hqwc.cn/news/314181.html

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

相关文章

LoadRunner安装,以及注意的点

摘要 要先安装主包&#xff0c;然后再去汉化&#xff0c;最后是去破解 因为LR12的虚拟用户数比较少&#xff0c;所以以下是以LR11教大家如何安装&#xff0c;因为LR11的虚拟用户数没有受到限制。 还有就是LR12与LR11区别不是很大。 但是LR11对Win10的兼容性不好&#xff0c…

【栈】根据模式串构造最小数字

import java.util.ArrayDeque; import java.util.Deque;/*** 思路&#xff1a;如果是字符‘I’直接对应的数字加入结果res中&#xff0c;如果是‘D’将对应的数字加入栈中。* 再次遇到‘I’先将对应的数字加入结果res中&#xff0c;然后再将栈中的元素从栈顶取出存放在* …

半导体行业-SECS/GEM协议 JAVA与SECS/GEM通信 什么是配方?springboot集成SECS通信协议 配方管理S7FX

Java与SECS基础通信 Java实现SECS指令S2F17获取时间 Java实现SECS指令 S10F3 终端单个显示例子 Java实现SECS指令 S7FX配方管理 Java实现SECS指令 S5F1报警/取消报警上传 实例源码及DEMO请查阅 JAVA开发SECS快速入门资料&#xff0c;SECS S7F19 什么是半导体配方&…

打破成本壁垒,免费SSL证书为中小企业保驾护航

HTTPS&#xff0c;这个曾经看似遥远的技术词汇&#xff0c;如今已与我们每个人的网络生活息息相关。而实现HTTPS加密传输的关键一环——SSL证书&#xff0c;正以其独特的安全性能&#xff0c;为网站筑起一道坚实的防护墙。更令人惊喜的是&#xff0c;免费SSL证书服务已经到来&a…

CSS 缩减顶部动画

<template><!-- mouseenter"startAnimation" 表示在鼠标进入元素时触发 startAnimation 方法。mouseleave"stopAnimation" 表示在鼠标离开元素时触发 stopAnimation 方法。 --><!-- 容器元素 --><div class"container" mou…

张量操作与线性回归

一、张量的操作&#xff1a;拼接、切分、索引和变换 &#xff08;1&#xff09;张量拼接与切分 1.1 torch.cat() 功能&#xff1a;将张量按维度dim进行拼接 • tensors: 张量序列 • dim : 要拼接的维度 torch.cat(tensors, dim0, outNone)函数用于沿着指定维度dim将多个张量…

simulink代码生成(五)——ePWM模块初级应用

前面分别讲到了SCI及ADC的配置及使用&#xff0c;现在梳理一下ePWM的配置和使用&#xff1b; 先打一些基础的DSP28335的基础知识&#xff1b; F28335 关于ePWM中断与SOC采样信号的一些思考_socasel-CSDN博客 F28335 ePWM模块简介——TMS320F28335学习笔记&#xff08;四&…

010、切片

除了引用&#xff0c;Rust还有另外一种不持有所有权的数据类型&#xff1a;切片&#xff08;slice&#xff09;。切片允许我们引用集合中某一段连续的元素序列&#xff0c;而不是整个集合。 考虑这样一个小问题&#xff1a;编写一个搜索函数&#xff0c;它接收字符串作为参数&a…

TecoGAN视频超分辨率算法

1. 摘要 对抗训练在单图像超分辨率任务中非常成功&#xff0c;因为它可以获得逼真、高度细致的输出结果。因此&#xff0c;当前最优的视频超分辨率方法仍然支持较简单的范数&#xff08;如 L2&#xff09;作为对抗损失函数。直接向量范数作损失函数求平均的本质可以轻松带来时…

【Unity入门】RequireComponent的使用

目录 RequireComponent的作用构造函数 RequireComponent的作用 RequireComponent 属性自动将所需的组件添加为依赖项。 当某个脚本必须依赖其他脚本或者组件共同使用时&#xff0c;为了避免人为添加过程的操作失误&#xff0c;可以在代码中使用RequireComponent&#xff0c;它…

CSS-4

平面转换 整体认识 div {margin: 100px 0;width: 100px;height: 100px;background-color: pink;/* 过渡效果 */transition: all 1s;}/* 当鼠标悬停到div时&#xff0c;进行平面转换 */div:hover {transform: translate(800px) rotate(360deg) scale(2) skew(180deg);}作用&…

单字符检测模型charnet使用方法,极简

Git链接 安装按照上面的说明&#xff0c;说下使用。 把tools下面的test做了一点修改&#xff0c;可以读取一张图片&#xff0c;把里面的单个字符都检测和识别出来。 然后绘制到屏幕上。 import torch from charnet.modeling.model import CharNet import cv2, os import num…