一. 主机字节序和网络字节序
32位机器CPU一次至少装载4字节, 这4字节在内存中的排列顺序就是字节序
字节序分为大端字节序: 低地址存高位
小端字节序:低地址存低位
利用union验证本机的字节序:
int main(){
union {
char a;
int b;
} test;
test.b = 1;
if (test.a == 0) {
printf("big endian\n"); //大端字节序
}
else if (test.a == 1){
printf("little endian\n"); //小端字节序
}
return 0;
}
原理:
现代PC大多采用小端字节序, 因此小端字节序又被成为主机字节序
规定网络字节序为大端字节序, 所有主机收发数据时要转换为大端字节序
// Linux 提供 4 个函数完成字节序转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
// n net代表网络, h host代表主机
// l long 32位, 用于 ip地址转换
// s short 16位 用于 端口转换
二. socket地址表示
通用socket
struct sockaddr
{
sa_family_t sa_family; // 地址族类型
char sa_data[14]; // 存放socket地址值
}
专用socket
struct sockaddr_in
{
sa_family_t sin_family; // 地址族
uint16_t sin_port; // 端口号
struct in_addr sin_addr;// Ipv4 地址结构体
}
struct in_addr
{
uint32_t s_addr; // Ipv4 地址
}
写代码用sockaddr_in, 类型转换为 sockaddr
三. ip地址转换函数
通常使用ip地址用点分十进制表示, 在编写代码的时候, 需要把ip转换为32位整数
#include <arpa/inet.h>
in_addr_t inet_addr (const char* strptr);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
四. TCP编程流程
TCP协议: 有连接, 可靠, 面向字节流
1. 创建 socket int socket(int domain, int type, int protocol);
2. 绑定地址信息(服务端) int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
3. 监听socket()服务端) int listen(int sockfd, int backlog);
backlog决定了内核中已完成连接队列的最大结点数4. 接受连接(服务端) int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
调用accept从listen监听队列中接受一个连接, accept成功返回一个新的连接socket, 可通过新socket来与请求连接的
客户端通信
5. 发起连接(客户端) int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端调用connect主动发起连接
6. 关闭连接 int close(int fd);
close 并非立即关闭一个连接, 而是将 fd 的引用计数 -1, 只有当 fd 为 0 时, 才真正关闭连接
在多进程程序中, 子进程拷贝了父进程地址空间, 只有父子进程都close了socket, 才能关闭连接
如果一定要立即终止连接, 使用
int shutdown(int sockfd, int how);
how: SHUT_RD 关闭读, SHUT_WR 关闭写, SHUT_RDWR 关闭读写
TCP数据读写 :
对文件的读写操作 read和write同样适用于socket, socket编程接口也提供了专门用于socket数据读写的系统调用
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv返回 - 1 出错, 返回大于0 表示实际读取的字节数, 返回 0 表示对方已经断开连接
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sned返回实际写入的字节长度
flags 一般置 0 , MSG_OOB 发送或接受紧急数据, MSG_PEEK 窥探读缓存中的数据, 此次操作不会删除数据
如何判断连接已经断开:
原理:
tcp的连接管理中, 内建有保活机制: 当长时间 没有数据往来时, 每隔一段时间都会向对方发送一个保活探测包, 要求对方回复
当多次发送的保活探测包都没有响应, 则认为连接断开
编写代码:
连接断开, recv 返回为 0 ; send会触发异常SIGPIPE(导致进程退出)
五. 封装TCP常用操作
class TcpSocket{
public:
TcpSocket():_fd(-1){}
~TcpSocket(){
}
public:
// 创建套接字
bool Socket(){
_fd = socket(AF_INET, SOCK_STREAM, 0);
if (_fd == -1){
cout << "Socket err" << endl;
return false;
}
return true;
}
//关闭套接字
bool Close() {
close(_fd);
cout << "close fd: " << _fd << endl;
return true;
}
// 绑定 ip port
bool Bind(string& ip, uint16_t port){
sockaddr_in bindAddr;
bindAddr.sin_family = AF_INET;
bindAddr.sin_port = htons(port);
bindAddr.sin_addr.s_addr = inet_addr(ip.c_str());
if (bind(_fd, (sockaddr*)&bindAddr, sizeof bindAddr) == -1){
cout << "Bind err" << endl;
return false;
}
return true;
}
// 监听套接字
bool Listen(int num){
if (listen(_fd, num) == -1){
cout << "listen err" << endl;
return false;
}
return true;
}
// 服务器等待连接
bool Accept(TcpSocket& newSock, string* ip = nullptr, uint16_t* port = nullptr){
sockaddr_in peerAddr;
socklen_t sockLen = sizeof peerAddr;
int newFd = accept(_fd, (sockaddr*)&peerAddr, &sockLen);
if (newFd < 0){
cout << "accept err" << endl;
return false;
}
newSock.SetFd(newFd);
if (ip != nullptr){
*ip = inet_ntoa(peerAddr.sin_addr);
}
if (port != nullptr) {
*port = ntohs(peerAddr.sin_port);
}
return true;
}
bool Connect(string& ip, uint16_t port){ // 客户端连接
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
servAddr.sin_addr.s_addr = inet_addr(ip.c_str());
if (connect(_fd, (sockaddr*)&servAddr, sizeof servAddr) == -1){
cout << "Connect() error" << endl;
return false;
}
return true;
}
bool Recv(string& msg){ //接受
char buf[4096] = {0};
//这里有问题: tcp流无数据边界, 不一定会一次接收完
int recvSize = recv(_fd, buf, 4096, 0);
if (recvSize < 0){
cout << "recv() err" << endl;
return false;
}
if (recvSize == 0){
return false;
}
msg.assign(buf, recvSize);
return true;
}
bool Send(string& msg){ // 发送
int ret = send(_fd, msg.c_str(), msg.size(), 0);
if (ret < 0){
cout << "send() err" << endl;
return false;
}
return true;
}
public:
void SetFd(int fd){
_fd = fd;
}
int GetFd(){
return _fd; // socket 文件描述符
}
private:
int _fd;
};
使用封装的接口实现简单的回声客户端 和 回声服务器(单对单):
服务端:
int main(){
TcpSocket tcp;
tcp.Socket();
string ip = "192.168.30.145";
tcp.Bind(ip, 14396);
tcp.Listen(5);
// 服务器一直运行, 等待客户端连接
while(1) {
TcpSocket newSock;
string clntIp;
uint16_t clntPort;
// 没有客户端连接, 将一直在这里阻塞
tcp.Accept(newSock, &clntIp, &clntPort);
cout << "Connect: (" << clntIp << "--" << clntPort << ")" << endl;
// 持续与连接的客户端收发信息
for(;;){
string msg;
if (!newSock.Recv(msg)){
newSock.Close();
break;
}
cout << "recv: " << msg << endl;
newSock.Send(msg);
}
}
tcp.Close();
return 0;
}
客户端:
int main(){
TcpSocket tcp;
tcp.Socket();
string ip = "192.168.30.145";
tcp.Connect(ip, 14396); // 连接设置ip 端口的客户端
// 循环收发消息
while(1) {
string msg;
getline(cin, msg);
tcp.Send(msg);
string resp;
tcp.Recv(resp);
cout << "recv msg: " << msg << endl;
}
tcp.Close();
return 0;
}
同一时刻, 只能有一个客户端与服务器通信:
如果要让服务器端处理多个客户端连接请求:可以使用多线程或多进程, 父进程(主线程) 处理连接请求,
与客户端的通信由子进程(子线程)实现.