目录
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_INET | lPv4协议族 | AF_INET | lPv4地址族 |
PF_INET6 | lPv6协议族 | AF_INET6 | lPv6地址族 |
宏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