1. 网络字节序和主机字节序转化
-
大端字节序:也称网络字节序,高位字节存储在内存的低地址处,低位字节存储在高地址处
-
小端字节序:也称主机字节序,低位字节存储在内存的低地址处,高位字节存储在高地址处
1.1 相关函数
-
将无符号短整数hostshort从主机字节顺序转换为网络字节顺序
-
uint16_t htons(uint16_t hostshort);
-
-
将无符号短整数netshort从网络字节顺序转换为主机字节顺序
-
uint16_t ntohs(uint16_t netshort);
-
struct in_addr{
uint32_t s_addr; /* 网络字节顺序中的地址 */
}
// 此结构体包含上面结构体
struct sockaddr_in{
sa_family_t sin_family; /* 地址族:AF_INET */
in_port_t sin_port; /* 端口号,网络字节顺序 */
struct in_addr sin_addr; /* 互联网地址 */
}
-
将来自 IPv4 点分十进制表示法的 Internet 主机地址 cp 转换为二进制形式(以网络字节顺序)并将其存储在 inp 指向的结构体中。
-
int inet_aton(const char *cp, struct in_addr *inp);
-
返回值:成功返回1,失败返回0
-
-
字符串格式转换为sockaddr_in格式
-
int inet_pton(int af, const char *src, void *dst);
-
int af:通常为 AF_INET 用于IPv4地址,或 AF_INET6 用于IPv6地址
-
const char *src:包含IP地址字符串的字符数组
-
void *dst:指向一个足够大的缓冲区
-
返回值:成功返回0,输入错误地址返回1,发生错误返回-1
-
2. TCP
2.1 TCP连接的建立和释放
三次握手
1.客户端发送SYN标志位1(表示这是一个同步报文)的报文,请求建立连接,这一过程为主动打开
2.服务端确认客户端的SYN,发送一个ACK和SYN标志均为1的报文
3.客户端确认服务端的报文,发送ACK标志为1的报文
四次挥手
1.客户端发送FIN标志为1的报文,请求断开连接,这一过程为主动关机
2.服务端确认客户端的FIN报文,客户端接收到第二次挥手信号,切换为FIN-WAIT-2状态 第二次挥手服务端仍可以发送数据
3.服务端发送FIN报文,这一过程称为被动关闭
4.客户端确认服务端发送的FIN报文,服务器接收到第四次挥手切换为CLOSEN状态 客户端发送第四次挥手信号后切换为TIME-WAIT状态,开始计时,等待2MSL(2倍最大报文时长,约定值)后进入CLOSEN状态
2.2 TCP所需用到的函数
-
2.2.1 socket
-
套接定义:允许不同主机上的进程通过网络进行数据交换,提供发送和接收数据的能力
-
-
套接字组成
-
网络地址:通常是IP地址,用于标识网络上的设备
-
端口号:用于标识设备上的特定应用或进程,是一个16位数字
-
协议:如TCP或UDP,定义数据传输的规则和格式
-
-
套接字类型:流套接字(TCP)和数据报套接字(UDP)
-
创建套接字
-
int socket(int domain,int type,int protocol);
-
int domain:指定要创建套接字的通信域 一般写AF_INFT(IPv4互联网协议)
-
int type:指定要创建的socket类型,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)
-
int protocol:补充协议 推荐写0
-
返回值:成功返回文件描述符 失败-1
-
-
2.2.2 绑定地址和端口号
-
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
-
int sockfd:套接字文件描述符
-
const struct sockaddr *addr:结构体,指定的地址、地址长度和格式取决于socket地址族
-
socklen_t addrlen:指定addr指向的结构体长度
-
返回值:成功0 失败-1
-
-
2.2.3 服务端进入监听状态
-
int listen(int sockfd,int backlog);
-
int sockfd:套接字文件描述符
-
int backlog:指定还未accpet但是已经完成链接的队列长度
-
返回值:成功0 失败-1
-
-
2.2.4 服务端提取第一个连接请求,创建新套接字
-
int accept(int sockfd,struct sockaddr * addr,socklen_t *addrlen)
-
int sockfd:套接字文件描述符
-
const struct sockaddr *addr:结构体,指定的地址、地址长度和格式取决于socket地址族
-
socklen_t addrlen:指定addr指向的结构体长度
-
返回值:成功0 失败-1
-
-
2.2.5 客户端调用,与服务器建立连接
-
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
-
int sockfd:客户端套接字文件描述符
-
const struct sockaddr *addr:指向sockaddr结构体指针,包含目的地址信息
-
socklen_t addrlen:指定addr指向的结构体长度
-
返回值:成功0 失败-1
-
-
2.2.6 传输信息
-
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
-
int sockfd:发送套接字文件描述符
-
const void *buf:发送缓冲区 const修饰为"只读"
-
size_t len:要发送的数据的字节长度
-
int flags:通常设置为0
-
返回值:成功返回发送的字节数,失败-1
-
-
2.2.7 接收信息
-
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
-
int sockfd:套接字文件描述符
-
void *buf:接收缓冲区
-
size_t len:缓冲区长度
-
int flags:通常设置为0
-
返回值:成功返回接收到的字节数,失败-1
-
-
2.2.8 关闭一部分套接字或全部连接
-
int shutdown(int sockfd,int how);
-
int sockfd:套接字文件描述符
-
int how:指定关闭的类型
-
返回值:成功0 失败-1
-
-
2.2.9 直接全部关闭
-
int close(int __fd);
-
int __fd:要关闭的文件描述符
-
返回值:成功0 失败-1
-
3. 示例代码(单个服务器和单个客户端)
3.1 服务端
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_client(void *arg)
{
int client_fd = *(int *)arg;
char *read_buff = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if(!read_buff)
{
perror("read_buff");
return NULL;
}
// 只要接收到消息就一直挂着
while(count = recv(client_fd,read_buff,1024,0))
{
if(count < 0){
perror("recv");
}
// 将接收到的数据打印到控制台
fputs(read_buff,stdout);
}
printf("客户端请求连接\n");
free(read_buff); // 释放内存
return NULL;
}
void *write_to_client(void *arg)
{
int client_fd = *(int *)arg;
char *write_buff = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if(!write_buff)
{
perror("write_buff");
return NULL;
}
// 获取控制台输入的数据
while(fgets(write_buff,1024,stdin)){
//发送数据
count = send(client_fd,write_buff,1024,0);
if(count < 0)
{
perror("send");
}
}
printf("接收到控制台的关闭请求 不再写入 关闭连接\n");
// 可以具体到某一端
shutdown(client_fd,SHUT_WR);
free(write_buff);
return NULL;
}
int main(int argc, char const *argv[])
{
struct sockaddr_in server_addr,client_addr;
int sockfd,temp_result,client_fd;
pthread_t read,write;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 设置服务端的IP和端口 使用ipv4
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(6666);
// TCP服务端编程步骤
// 1.创建socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2.绑定端口和ip
temp_result = bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
handle_error("bind",temp_result);
// 3.进入监听状态
temp_result = listen(sockfd,1024);
handle_error("listen",temp_result);
// 4.与客户端获得连接
socklen_t client_len = sizeof(client_addr);
client_fd = accept(sockfd,(struct sockaddr *)&client_addr,&client_len);
handle_error("accept",client_fd);
printf("与客户端%s %d建立连接 文件描述符%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port),client_fd);
// 创建两个线程 一个读 一个写
pthread_create(&read,NULL,read_from_client,(void *)&client_fd);
pthread_create(&write,NULL,write_to_client,(void *)&client_fd);
// 阻塞主线程 等待子线程运行完成
pthread_join(read,NULL);
pthread_join(write,NULL);
// 释放资源
close(sockfd);
close(client_fd);
return 0;
}
3.2 客户端
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#define handle_error(cmd,result) \
if(result < 0) \
{ \
perror(cmd); \
return -1; \
}
void *read_from_server(void *arg)
{
int client_fd = *(int *)arg;
char *read_buff = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if(!read_buff)
{
perror("read_buff");
return NULL;
}
// 只要接收到消息就一直挂着
while(count = recv(client_fd,read_buff,1024,0))
{
if(count < 0){
perror("recv");
}
// 将接收到的数据打印到控制台
fputs(read_buff,stdout);
}
printf("服务端请求关闭\n");
free(read_buff); // 释放内存
return NULL;
}
void *write_to_server(void *arg)
{
int client_fd = *(int *)arg;
char *write_buff = malloc(sizeof(char) * 1024);
ssize_t count = 0;
if(!write_buff)
{
perror("write_buff");
return NULL;
}
// 获取控制台输入的数据
while(fgets(write_buff,1024,stdin)){
//发送数据
count = send(client_fd,write_buff,1024,0);
if(count < 0)
{
perror("send");
}
}
printf("接收到控制台的关闭请求 不再写入 关闭连接\n");
// 可以具体到某一端
shutdown(client_fd,SHUT_WR);
free(write_buff);
return NULL;
}
int main(int argc, char const *argv[])
{
struct sockaddr_in server_addr,client_addr;
int sockfd,temp_result,client_fd;
pthread_t read,write;
// 清空
memset(&server_addr,0,sizeof(server_addr));
memset(&client_addr,0,sizeof(client_addr));
// 设置客户端的IP和端口 使用ipv4
client_addr.sin_family = AF_INET;
inet_pton(AF_INET,"自己的IP(虚拟机)",&client_addr.sin_addr);
client_addr.sin_port = htons(8888);
// 设置服务端的IP和端口 使用ipv4
server_addr.sin_family = AF_INET;
inet_pton(AF_INET,"127.0.0.1",&server_addr.sin_addr);
server_addr.sin_port = htons(6666); //注意这里的端口号要和服务端一致
// TCP客户端编程步骤
// 1.创建socket
sockfd = socket(AF_INET,SOCK_STREAM,0);
handle_error("socket",sockfd);
// 2.绑定端口和ip
temp_result = bind(sockfd,(struct sockaddr *)&client_addr,sizeof(client_addr));
handle_error("bind",temp_result);
// 3.主动连接服务器
temp_result = connect(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr));
handle_error("connect",temp_result);
printf("连接上服务器%s %d\n",inet_ntoa(server_addr.sin_addr),ntohs(server_addr.sin_port));
// 创建两个线程 一个读 一个写
pthread_create(&read,NULL,read_from_server,(void *)&sockfd);
pthread_create(&write,NULL,write_to_server,(void *)&sockfd);
// 阻塞主线程 等待子线程运行完成
pthread_join(read,NULL);
pthread_join(write,NULL);
// 释放资源
close(sockfd);
close(client_fd);
return 0;
}
3.3 运行结果
客户端和服务端可以互相收发消息