文章目录
0. 字节序转换接口
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分.
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
- 套接字编程的头文件
#include <netinet/in.h> // struct_sockaddr_in结构体的定义以及协议的宏
#include <arpa/inet.h> // 字节序转换接口
#include <sys/socket.h> // 套接字接口
1. UDP
- UDP流程图
1.1 常用接口
1. 创建套接字
int socket(int domain, int type, int protocol);
- domain: 地址域, 选择使用哪种地址结构 IPv4、IPv6— AF_INET、AF_INET
- type : 套接字类型
流式套接字 : SOCK_STREAM, 提供字节流传输服务.有序的, 可靠地, 双向的, 基于连接的传输服务
数据报套接字 : SOCK_DGRAM, 提供数据报传输服务.无连接, 不可靠, 有最大长度限制的消息传输服务 - protocol: 传输时所使用的协议类型
0 : 不同套接字类型的默认协议, 数据报套接字 : 默认UDP.流式套接字 : 默认TCP
IPPROTO_TCP : TCP协议
IPPROTO_UDP : UDP协议 - 返回值 : 返回一个文件描述符作为操作句柄; 失败返回 - 1
2. 为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
- sockfd : 创建套接字返回的操作句柄
- addr : 地址信息结构, 用于描述内核中socket所使用的地址信息
- bind接口绑定地址信息的时候, 用户使用自己定义所使用的地址结构(需强制类型转化), 进入函数后会进行判断, 针对不同的地址结构做不同的事, 并且socket结构体中只会绑定本机的地址信息
函数的参数是sockaddr, 使用时传入sockaddr_in 或 sockaddr_in6, 通过强转即可使用
- addrlen : 地址信息的长度-- - bind接口是统一的地址绑定接口, 但是地址结构多种多样长度不一.
- 返回值 : 成功返回0, 失败返回 - 1
注意 : 一般用户端不主动去绑定本机的信息, 因为地址会自动绑定合适的, 而自己绑定可能会造成端口冲突
3. 发送数据
ssize_t sendto(int sockfd, char* data, int data_len, int flag, struct sockaddr*dest_addr, socklen_t addr_len);
- sockfd : 指定内核中的socket结构体
- data : 要发送的数据首地址
- data_len : 要发送的数据长度
- flag : 选项参数, 默认为0表示若缓冲区数据已满则阻塞发送数据并等待
- dest_addr : 目的端的首地址
- addr_len : 地址长度
- 返回值 : 成功返回实际发送的数据字节长度, 失败返回 - 1
4. 接受数据
int recvfrom(int sockfd, char* buf, int buf_len, int flag, struct sockaddr* peer_addr, socklen_t* addr_len);
- sockfd : 指定内核中的socket结构体, 从哪个socket的接受缓冲区取出数据
- buf : 用户态缓冲区, 用于存放从内核拷贝出来的数据
- buf_len : 想要获取的数据长度, 此长度不能大于buf的长度
- flag : 默认为0阻塞接受, 缓冲区没有数据则阻塞等待
- peer_addr : 输出型参数 地址去区首地址, 用于获取发送这个数据的源端地址信息得到数据的发送者
- add_len : 输入输出型参数, 用于指定想要获取多长的地址信息, 并用于返回实际获取的地址长度
- 返回值 : 成功返回实际接收到的数据长度, 失败返回 - 1
5. 关闭套接字
- int close(int fd);
1.2 UDP程序示例
1.2.1 头文件
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
class UDPServer
{
public:
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sockfd < 0)
{
perror("socket error");
return -1;
}
return true;
}
// 用来将端口信息从主机字节序转换成网络字节序并绑定到sockaddr中
void Addr(struct sockaddr_in* addr, const string ip, const uint16_t port)
{
addr->sin_family = AF_INET;
addr->sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr->sin_addr.s_addr);
}
bool Bind(const string ip, const uint16_t port)
{
struct sockaddr_in addr;
Addr(&addr, ip, port);
socklen_t len = sizeof(sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr,len);
if (ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Recv(string& buf, string& ip, uint16_t& port)
{
char tmp[4096] = {0};
struct sockaddr_in addr;
socklen_t len = sizeof(sockaddr_in);
int ret = recvfrom(_sockfd, tmp, 4096, 0, (struct sockaddr*)&addr, &len);
if (ret < 0)
{
perror("recvfrom error");
return false;
}
ip = inet_ntoa(addr.sin_addr);
port = ntohs(addr.sin_port);
buf.assign(tmp, ret);
return true;
}
bool Send(const string& data, const string& ip, const uint16_t port)
{
struct sockaddr_in addr;
Addr(&addr, ip, port);
socklen_t len = sizeof(sockaddr_in);
int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("sendto error");
return false;
}
return true;
}
bool Close()
{
close(_sockfd);
return true;
}
private:
int _sockfd;
};
1.2.2 服务端
int main(int argc, char* argv[])
{
if (argc != 3)
{
cout << "请输入正确的格式./udpsrv ip 端口" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
UDPServer sev;
sev.Socket();// 创建套接字
sev.Bind(ip, port); // 绑定信息
while (1)
{
string buf;
string revip;
uint16_t revport;
sev.Recv(buf, revip, revport);
cout << buf << endl;
cin >> buf;
sev.Send(buf, revip, revport);
}
return 0;
}
1.2.3 客户端
int main(int argc, char* argv[])
{
if (argc != 3)
{
cout << "请输入正确的格式./udpsrv ip 端口" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
UDPServer sev;
sev.Socket();// 创建套接字
// sev.Bind(ip, port); // 绑定信息
while (1)
{
string buf;
cin >> buf;
sev.Send(buf, ip, port);
sev.Recv(buf, ip, port);
cout << buf << endl;
}
return 0;
}
2. TCP
TCP流程图:
- 客户端详解
由于TCP是面向连接的, 并且使用监听将请求的保存客户端地址信息的新创建的socket存放到队列中, 因此需要使用多线程或多进程来执行交互.
2.1 常用接口
1.创建套接字
int socket(int domain, int type, int protocol);
- domain : 地址域, 选择使用哪种地址结构 IPv4、IPv6-- - AF_INET、AF_INET
- type : 套接字类型
流式套接字 : SOCK_STREAM, 提供字节流传输服务.有序的, 可靠地, 双向的, 基于连接的传输服务
数据报套接字 : SOCK_DGRAM, 提供数据报传输服务.无连接, 不可靠, 有最大长度限制的消息传输服务 - protocol : 传输时所使用的协议类型
0 : 不同套接字类型的默认协议, 数据报套接字 : 默认UDP.流式套接字 : 默认TCP
IPPROTO_TCP : TCP协议
IPPROTO_UDP : UDP协议 - 返回值 : 返回一个文件描述符作为操作句柄; 失败返回 - 1
2.为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
- sockfd : 创建套接字返回的操作句柄
- addr : 地址信息结构, 用于描述内核中socket所使用的地址信息
bind接口绑定地址信息的时候, 用户使用自己定义所使用的地址结构(需强制类型转化), 进入函数后会进行判断, 针对不同的地址结构做不同的事, 并且socket结构体中只会绑定本机的地址信息
函数的参数是sockaddr, 使用时传入sockaddr_in 或 sockaddr_in6, 通过强转即可使用 - addrlen : 地址信息的长度-- - bind接口是统一的地址绑定接口, 但是地址结构多种多样长度不一.
- 返回值 : 成功返回0, 失败返回 - 1
注意 : 一般用户端不主动去绑定本机的信息, 因为地址会自动绑定合适的, 而自己绑定可能会造成端口冲突
3.开始监听
int listen(int sockfd, int backlog);
- sockfd : 套接字操作句柄
- backlog : 在TCP套接字中backlog的含义在Linux 2.2中已经改变.它指定了已经完成连接正等待应用程序接收的套接字队列的长度, 而不是未完成连接的数目.未完成连接套接字队列的最大长度可以使用tcp_max_syn_backlog sysctl设置当打开syncookies时不存在逻辑上的最大长度, 此设置将被忽略.并且实际长度为backlog + 1.
在网络通信中, 客户端通常处于主动的一方, 而服务器则是被动的一方, 服务器是被连接的, 所以他要时刻准备着被连接, 所以就需要调用 listen() 来监听, 等着被连接.
listen() 函数的主要作用就是将 socket() 函数得到的 sockfd 变成一个被动监听的套接字, 用来被动等待客户端的连接, 而参数 backlog 的作用就是设置正等待应用程序接收的连接队列的长度
三次握手, 建立连接不是 listen() 函数完成的, 而是内核完成的, listen() 函数只是将 sockfd 和 backlog 告诉内核, 然后就返回了
之后, 如果有客户端通过 connect() 发起连接请求, 内核就会通过三次握手建立连接, 然后将建立好的连接放到一个队列中, 这个队列称为 : 已完成连接队列
4.服务端获取从队列中取出的socket句柄
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
这一步的功能就是从内核的已完成连接队列中取出socket, 腾出一个位子可以继续接收新的连接.如果这个队列中已经没有已完成连接的套接字, 那么 accept() 就会一直阻塞, 直到取得一个已经建立连接的套接字
- sockfd : 描述符, 指定获取从哪个服务端socket所创建的新套接字.
- addr : 客户端的地址信息
- addrlen : 输入输出型参数, 指定想要获取的地址信息长度, 并获得实际得到的地址信息长度
- 返回值 : 返回具体新创建的套接字的操作句柄, 用于后续与指定客户端进行通信, 失败返回 - 1
5.客户端请求连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd : 建立的socket
- addr : 服务端的地址信息
- addrlen : 对端地址信息长度
- 返回值 : 返回具体新创建的套接字的操作句柄, 用于后续与指定服务端进行通信, 失败返回 - 1
7.发送和接受信息
int recv(int sockfd, char* buf, int len, int flag);
int send(int sockfd, char* buf, int len, int flag);
- sockfd : 本地的socket
- flag : 一般为0
8.关闭套接字
int close(int fd);
2.2 TCP程序示例
2.2.1 头文件
#include <iostream>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using std::cout;
using std::endl;
using std::string;
#define CHECK_RET(func) if ((func) == false) {return -1;}
const int BACKLOG = 3;
class TcpSocket
{
public:
TcpSocket()
: _sockfd(-1)
{ }
int getFd()
{
return _sockfd;
}
void setFd(int fd)
{
_sockfd = fd;
}
bool Socket()
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
void Addr(sockaddr_in& addr, const string& ip, const uint16_t port)
{
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);
}
bool Bind(const string& ip, const uint16_t port)
{
struct sockaddr_in addr;
Addr(addr, ip, port);
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = BACKLOG)
{
int ret = listen(_sockfd, backlog);
if (ret < 0)
{
perror("listen error");
return false;
}
return true;
}
bool Connect(const string& ip, const uint16_t port)
{
struct sockaddr_in addr;
Addr(addr, ip, port);
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0)
{
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket& sock, string* ip = NULL, uint16_t* port = NULL)
{
struct sockaddr_in cliaddr;
socklen_t len = sizeof(struct sockaddr_in);
int newfd = accept(_sockfd, (struct sockaddr*)&cliaddr, &len);
if (newfd < 0)
{
perror("accept error");
return false;
}
sock._sockfd = newfd;
if (ip != NULL)
{
*ip = inet_ntoa(cliaddr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(cliaddr.sin_port);
}
return true;
}
bool Recv(string& buf)
{
char tmp[4096] = { 0 };
ssize_t ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0)
{
perror("recv perror");
return false;
}
else if (ret == 0)
{
printf("connection break\n");
return false;
}
buf.assign(tmp);
return true;
}
bool Send(string& data)
{
ssize_t ret = send(_sockfd, data.c_str(), data.size(), 0);
if (ret < 0)
{
perror("send error");
return false;
}
return true;
}
bool Close()
{
close(_sockfd);
_sockfd = -1;
return true;
}
private:
int _sockfd;
};
2.2.2 服务端多进程版
#include "TcpSocket.hpp"
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
#include <cstdlib>
using std::cin;
void sigcb(int signo)
{
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
cout << "em: ./tcp_srv ip prot" << endl;
return -1;
}
signal(SIGCHLD, sigcb);
string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket mainSock;
CHECK_RET(mainSock.Socket());
CHECK_RET(mainSock.Bind(ip, port));
CHECK_RET(mainSock.Listen());
while (1)
{
TcpSocket newSock;
string cli_ip;
uint16_t cli_port = 0;
bool ret = mainSock.Accept(newSock, &cli_ip, &cli_port);
if (ret == false)
{
cout << "mainSock Accept erorr" << endl;
continue;
}
cout << "new cli: " << cli_ip << " " << cli_port << endl;
pid_t pid = fork();
if (pid == 0)
{
// 子进程负责循环跟客户端进行通信
while (true)
{
string buf;
int r = newSock.Recv(buf);
if (r == -1)
{
cout << "recv ret == -1" << endl;
continue;
}
cout << "client say: " << buf << endl;
if (buf == "end")
{
// 结束会话
break;
}
cout << "server say: ";
cin >> buf;
newSock.Send(buf);
}
cout << "此通信结束" << endl;
newSock.Close();
exit(0);
}
newSock.Close();
}
cout << "主套接字关闭" << endl;
mainSock.Close();
return 0;
}
2.2.3 服务端多线程版
#include "TcpSocket.hpp"
#include <cstdlib>
void *thr_start(void *arg)
{
long sockfd = (long)arg;
TcpSocket newsock;
newsock.setFd(sockfd);
while(1) {
std::string buf;
bool ret = newsock.Recv(buf);
if (ret == false) {newsock.Close(); continue;}
printf("client say: %s\n", buf.c_str());
printf("server say:");
fflush(stdout);
buf.clear();
std::cin >> buf;
ret = newsock.Send(buf);
if (ret == false) {newsock.Close(); continue;}
}
newsock.Close();
return NULL;
}
int main(int argc, char* argv[])
{
if (argc != 3) {
printf("em: ./tcp_srv host_ip host_port\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket()); //创建套接字
CHECK_RET(lst_sock.Bind(ip, port));//绑定地址信息
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket newsock;
std::string cli_ip;
uint16_t cli_port;
bool ret = lst_sock.Accept(newsock,&cli_ip, &cli_port);//获取新连接
if (ret == false) {
continue;//服务端不会因为一次获取的失败而退出,而是继续重新获取下一个
}
printf("new conn:[%s:%d]\n", cli_ip.c_str(), cli_port);
pthread_t tid;
pthread_create(&tid, NULL, thr_start, (void*)newsock.getFd());
pthread_detach(tid);//线程退出后直接自动释放资源
}
lst_sock.Close();
return 0;
}
2.2.3 客户端
#include "TcpSocket.hpp"
#include <signal.h>
#include <cstdlib>
using std::cin;
void sigcb(int signo)
{
cout << "connect break" << endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
cout << "em: ./tcp_cli srv_ip srv_port" << endl;
return -1;
}
signal(SIGPIPE, sigcb);
string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket cli_sock;
CHECK_RET(cli_sock.Socket());
CHECK_RET(cli_sock.Connect(ip, port));
while (true)
{
string buf;
cout << "client say: ";
cin >> buf;
cli_sock.Send(buf);
if (buf == "end")
{
break;
}
cli_sock.Recv(buf);
cout << "server say: " << buf << endl;
}
cli_sock.Close();
return 0;
}
3. HTTP
- http的底层时tcp, 故直接使用tcp的头文件
#include "TcpSocket.hpp"
#include <signal.h>
#include <cstdlib>
#include <sys/wait.h>
#include <cstdlib>
#include <sstream>
using std::cin;
int main(int argc, char* argv[])
{
if (argc != 3)
{
cout << "em: ./tcp_srv ip prot" << endl;
return -1;
}
string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket mainSock;
CHECK_RET(mainSock.Socket());
CHECK_RET(mainSock.Bind(ip, port));
CHECK_RET(mainSock.Listen());
TcpSocket newsock;
bool ret = mainSock.Accept(newsock);
if (ret == false)
{
cout << "mainSock Accept erorr" << endl;
}
// 子进程负责循环跟客户端进行通信
std::string buffer;
newsock.Recv(buffer);
std::cout << "http req:[" << buffer << "]\n";
std::string body = "<html><body><h1>Hello World</h1></body></html>";
std::string blank = "\r\n";
std::stringstream header;
header << "Content-Length: " << body.size() << "\r\n";
header << "Content-Type: text/html\r\n";
std::string first = "HTTP/1.1 504 OK\r\n";
string tmp = header.str();
newsock.Send(first);//发送首行信息
newsock.Send(tmp);//发送头部信息
newsock.Send(blank);//发送空行
newsock.Send(body);//发送正文
newsock.Close();
cout << "此通信结束" << endl;
newsock.Close();
cout << "主套接字关闭" << endl;
mainSock.Close();
return 0;
}
- 编译执行
注意执行前关闭防火墙
[test@localhost tcptest]$ systemctl stop firewalld.service
[test@localhost tcptest]$ ./http_srv 192.168.66.129 9000
- 效果