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
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转换为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协议: 有连接, 可靠, 面向字节流
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(导致进程退出)
服务端:
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; }同一时刻, 只能有一个客户端与服务器通信:
如果要让服务器端处理多个客户端连接请求:可以使用多线程或多进程, 父进程(主线程) 处理连接请求,
与客户端的通信由子进程(子线程)实现.
