服务器客户端模型:
client / server
brow / ser
b / s http
p2p
socket——tcp
1、模式 C/S 模式 ==》服务器/客户端模型
server :socket()-->bind()--->listen()-->accept()-->recv()-->close()
client :socket()-->connect()-->send()-->close();
int on = 1;
setsockopt(listfd, SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
listfd
:这是你想要设置选项的套接字的文件描述符。通常,这个套接字是通过调用socket()
函数创建的,用于监听传入的连接请求(例如,在服务器程序中)。SOL_SOCKET
:这是选项所在的级别。SOL_SOCKET
表示这些选项是套接字级别的,而不是特定于某个协议(如TCP或UDP)的。SO_REUSEADDR
:这是要设置的选项的名称。它允许在同一个本地地址和端口上启动多个套接字。这对于开发中的测试或者服务器程序快速重启而不必等待操作系统释放端口号特别有用。&on
:这是一个指向整数的指针,整数的值决定了选项的状态。在这个例子中,on
应该是一个之前被设置为非零值(通常是1)的整数,表示启用SO_REUSEADDR
选项。如果on
的值为0,则表示禁用此选项。sizeof(on)
:这是on
变量的大小,以字节为单位。这是告诉setsockopt
函数on
变量的长度,确保函数可以正确地读取它的值。
服务器端:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
1.int socket(int domain, int type, int protocol);
功能:程序向内核提出创建一个基于内存的套接字描述符
参数:domain 地址族,PF_INET == AF_INET ==>互联网程序
PF_UNIX == AF_UNIX ==>单机程
type 套接字类型:
SOCK_STREAM 流式套接字 ===》TCP
SOCK_DGRAM 用户数据报套接字===>UDP
SOCK_RAW 原始套接字 ===》IP
- SOCK_STREAM - 流式套接字:
- 这类套接字提供了面向连接的、可靠的、基于字节流的服务。它们通过TCP(传输控制协议)实现,确保数据按照发送的顺序到达接收方,并且没有数据丢失或重复。
- TCP协议在发送数据之前会先建立连接,并在数据传输结束后关闭连接。这种特性使得
SOCK_STREAM
非常适合需要可靠传输的应用,如网页服务器和客户端之间的通信。
- SOCK_DGRAM - 用户数据报套接字:
- 这类套接字提供的是无连接的、不可靠的、基于消息的服务。它们通过UDP(用户数据报协议)实现,不保证数据包的顺序、完整性或到达。
- UDP协议在发送数据之前不需要建立连接,每个数据包都是独立的,这使得
SOCK_DGRAM
非常适合对实时性要求高且能容忍数据丢失的应用,如视频流、实时游戏等。
- SOCK_RAW - 原始套接字:
- 原始套接字允许程序直接访问网络层(如IP层)的数据包。这意味着你可以发送和接收原始IP数据包,包括ICMP(Internet控制消息协议)数据包等。
- 由于
SOCK_RAW
套接字允许绕过传输层的封装,它们通常用于需要直接操作网络层协议或进行网络诊断的应用。然而,由于它们提供了较低级别的网络访问,因此使用时需要谨慎,以避免破坏网络协议或造成安全问题。
protocol 协议 ==》0 表示自动适应应用层协议。
返回值:成功 返回申请的套接字id
失败 -1;
2、int bind(int sockfd, struct sockaddr *my_addr,
socklen_t addrlen);
功能:
如果该函数在服务器端调用,则表示将参数1相关的文件描述符文件(sockfd)与参数2 指定的接口地址关联(addr),用于从该接口接受数据。
如果该函数在客户端调用,则表示要将数据从参数1所在的描述符中取出并从参数2所在的接口
设备上发送出去。
注意:如果是客户端,则该函数可以省略,由默认
接口发送数据。
参数:sockfd 之前通过socket函数创建的文件描述符,套接字id
my_addr 是物理接口的结构体指针。表示该接口的信息。
struct sockaddr 通用地址结构
{
u_short sa_family; 地址族
char sa_data[14]; 地址信息
};
转换成网络地址结构如下:
struct _sockaddr_in ///网络地址结构
{
u_short sin_family; 地址族
u_short sin_port; ///地址端口
struct in_addr sin_addr; ///地址IP
char sin_zero[8]; 占位
};
struct in_addr
{
in_addr_t s_addr;
}
socklen_t addrlen: 参数2 的长度。
返回值:成功 0
失败 -1;
3、 int listen(int sockfd, int backlog);(监听套接字)
功能:在参数1所在的套接字id上监听等待链接。
参数:sockfd 套接字id
backlog 允许链接的个数。
返回值:成功 0
失败 -1;
4、int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
功能:从已经监听到的队列中取出有效的客户端链接并
接入到当前程序。
参数:sockfd 套接字id
addr 如果该值为NULL ,表示不论客户端是谁都接入。
如果要获取客户端信息,则事先定义变量
并传入变量地址,函数执行完毕将会将客户端
信息存储到该变量中。
addrlen: 参数2的长度,如果参数2为NULL,则该值
也为NULL;
如果参数不是NULL,&len;
一定要写成len = sizeof(struct sockaddr);
返回值:成功 返回一个用于通信的新套接字id;
从该代码之后所有通信都基于该id
失败 -1;
5、接受函数:/发送函数:
read()/write () ///通用文件读写,可以操作套接字。
recv(,0) /send(,0) ///TCP 常用套机字读写
recvfrom()/sendto() ///UDP 常用套接字读写
ssize_t recv(int sockfd, void *buf, size_t len,
int flags);
功能:从指定的sockfd套接字中以flags方式获取长度
为len字节的数据到指定的buff内存中。
参数:sockfd (通信套接字)
如果服务器则是accept的返回值的新fd
如果客户端则是socket的返回值旧fd
buff 用来存储数据的本地内存,一般是数组或者
动态内存。
len 要获取的数据长度
flags 获取数据的方式,0 表示阻塞接受。
返回值:成功 表示接受的数据长度,
一般小于等于len
失败 -1;
6、close() ===>关闭指定的套接字id;
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // 包含inet_addr等函数 #include <string.h> #include <time.h> // 移除不必要的类型定义,直接使用标准类型 int main(int argc, char *argv[]) { // 监听套接字 int listfd = socket(AF_INET, SOCK_STREAM, 0); if (listfd == -1) { perror("socket"); exit(1); } struct sockaddr_in ser, cli; memset(&ser, 0, sizeof(ser)); // 使用memset代替bzero,bzero可能在某些系统上不可用 memset(&cli, 0, sizeof(cli)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); ser.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(listfd, (struct sockaddr *)&ser, sizeof(ser)) == -1) { // 明确类型转换 perror("bind"); exit(1); } // 监听连接,设置最大连接队列长度 listen(listfd, 3); socklen_t len = sizeof(cli); // 通信套接字 int conn = accept(listfd, (struct sockaddr *)&cli, &len); if (conn == -1) { perror("accept"); exit(1); } while (1) { char buf[512] = {0}; int rd_ret = recv(conn, buf, sizeof(buf), 0); if (rd_ret <= 0) { // 0 表示对方断开连接,-1 表示错误 break; } time_t tm; time(&tm); // 注意:ctime(&tm) 返回一个指向静态字符串的指针,重复使用可能导致数据覆盖 // 这里我们创建一个新的缓冲区来存储时间戳 char time_str[64]; strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&tm)); // 使用snprintf来安全地格式化字符串,避免缓冲区溢出 snprintf(buf, sizeof(buf), "%s %s", buf, time_str); send(conn, buf, strlen(buf), 0); } close(listfd); close(conn); return 0; // 修正了错误的函数体结束符 }
===================================================
客户端:
1、int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:该函数固定有客户端使用,表示从当前主机向目标
主机发起链接请求。
参数:sockfd 本地socket创建的套接子id
addr 远程目标主机的地址信息。
addrlen: 参数2的长度。
返回值:成功 0
失败 -1;
2、int send(int sockfd, const void *msg,
size_t len, int flags);
功能:从msg所在的内存中获取长度为len的数据以flags
方式写入到sockfd对应的套接字中。
参数:sockfd:
如果是服务器则是accept的返回值新fd
如果是客户端则是sockfd的返回值旧fd
msg 要发送的消息
len 要发送的消息长度
flags 消息的发送方式。
返回值:成功 发送的字符长度
失败 -1;
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> /* 提供了一些基本的数据类型 */ #include <sys/socket.h> /* 提供了对套接字(socket)的操作接口 */ #include <netinet/in.h> /* 提供了IP地址和端口号的转换函数 */ #include <netinet/ip.h> /* 提供了IP协议相关的定义,但在这个程序中可能未直接使用 */ #include <string.h> /* 提供了字符串操作函数 */ #include <time.h> /* 提供了时间处理的函数,但在这个程序中未直接使用 */ #include <arpa/inet.h> /* 提供了网络地址的转换函数,如inet_addr */ // typedef struct sockaddr* (SA); int main(int argc, char *argv[]) { // 创建一个TCP套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (-1 == sockfd) { perror("socket"); // 如果创建套接字失败,打印错误信息 exit(1); // 退出程序 } // 初始化服务器地址结构 struct sockaddr_in ser; bzero(&ser, sizeof(ser)); // 使用bzero清零结构体,但建议使用memset更通用 ser.sin_family = AF_INET; // 使用IPv4地址 ser.sin_port = htons(50000); // 设置端口号,htons用于主机字节序到网络字节序的转换 // 将字符串地址转换为网络字节序的地址 ser.sin_addr.s_addr = inet_addr("127.0.0.1"); // 连接到服务器 int ret = connect(sockfd, (SA)&ser, sizeof(ser)); // 注意:修正了SA的使用,应为struct sockaddr*类型 if (-1 == ret) { perror("connect"); // 如果连接失败,打印错误信息 exit(1); // 退出程序 } // 循环发送和接收消息 while (1) { char buf[512] = "hello,this is tcp test"; // 初始化发送缓冲区 send(sockfd, buf, strlen(buf), 0); // 发送消息 bzero(buf, sizeof(buf)); // 清空缓冲区以接收新消息 recv(sockfd, buf, sizeof(buf), 0); // 接收服务器响应 printf("buf :%s\n", buf); // 打印接收到的消息 sleep(1); // 等待1秒 } // 注意:由于存在无限循环,close(sockfd)和return 0;实际上不会被执行 // 如果需要退出程序,应该在循环中添加适当的退出条件 // 关闭套接字 // close(sockfd); // return 0; }
练习:
使用TCP完成文件的复制:
server.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <string.h> #include <time.h> #include <fcntl.h> typedef struct sockaddr* (SA); int main(int argc, char *argv[]) { //监听套接字 int listfd = socket(AF_INET,SOCK_STREAM, 0); if(-1 == listfd) { perror("socket"); exit(1); } struct sockaddr_in ser,cli; bzero(&ser,sizeof(ser)); bzero(&cli,sizeof(cli)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); //host to net long ser.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(listfd,(SA)&ser,sizeof(ser)); if(-1 == ret) { perror("bind"); exit(1); } //同一时刻三次握手排队数 listen(listfd,3); socklen_t len = sizeof(cli); //通信套接字 int conn = accept(listfd,(SA)&cli,&len); if(-1 == conn) { perror("accept"); exit(1); } int fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666); if(-1 ==fd) { perror("open"); exit(1); } while(1) { char buf[512]={0}; int rd_ret = recv(conn,buf,sizeof(buf),0); if(rd_ret<=0) {// 0 对方断开连接 -1 错误 break; } write(fd,buf,rd_ret); bzero(buf,sizeof(buf)); strcpy(buf,"123"); send(conn,buf,strlen(buf),0); } close(fd); close(listfd); close(conn); return 0; }
clin.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <string.h> #include <time.h> #include <arpa/inet.h> #include <fcntl.h> typedef struct sockaddr* (SA); int main(int argc, char *argv[]) { int sockfd = socket(AF_INET,SOCK_STREAM,0); if(-1 == sockfd) { perror("socket"); exit(1); } struct sockaddr_in ser; bzero(&ser,sizeof(ser)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); //host to net long ser.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect(sockfd,(SA)&ser,sizeof(ser)); if(-1 == ret) { perror("connect"); exit(1); } int fd = open("/home/linux/1.png",O_RDONLY); if(-1 ==fd) { perror("open"); exit(1); } while(1) { char buf[512]={0}; int rd_ret = read(fd,buf,sizeof(buf)); if(rd_ret<=0) { break; } send(sockfd,buf,rd_ret,0); bzero(buf,sizeof(buf)); recv(sockfd,buf,sizeof(buf),0); } close(fd); close(sockfd); return 0; }
TCP使用过程中会出现粘包的现象
TCP粘包是指在TCP协议传输过程中,发送方连续发送的多个小数据包在接收方被组合成一个较大的数据块,或者多个小数据包粘合在一起被接收的现象。这种现象通常是由于TCP的流式传输特性和网络传输的复杂性所导致的。以下是对TCP粘包的详细解析:
一、TCP粘包的原因
TCP的流式传输特性:TCP是一个面向流的协议,它不会保留消息的边界。在TCP看来,数据流是一串连续的无边界的字节流。因此,TCP传输层并不了解上层业务数据的具体含义,它只负责将数据按照TCP缓冲区的实际情况进行划分和重组。
缓冲区大小不一致:发送方和接收方的缓冲区大小可能不一致。当发送方发送的数据包小于TCP缓冲区的大小时,TCP可能会将多个小的数据包合并成一个大的数据包发送,以提高传输效率。接收方在接收数据时,可能无法准确地按照发送方的发送边界来接收数据包,从而导致粘包现象。
接收方处理不及时:如果接收方用户进程不及时从系统接收缓冲区中取走数据,当新的数据包到达时,它会被放置在接收缓冲区的末尾,从而导致多个数据包粘合在一起。
二、TCP粘包的表现
在接收方看来,粘包现象表现为一次性接收到多个数据包的内容,这些数据包在逻辑上应该是分开的,但在实际接收时却粘合在一起。这可能会导致接收方无法正确地解析和处理数据。
三、TCP粘包的解决方法
为了解决TCP粘包问题,可以采取以下几种方法:
消息边界:在发送的数据中增加消息边界,如在数据包之间添加特定的分隔符(如换行符、特殊字符等)。接收方根据消息边界来区分和解析每个数据包。
固定长度:固定每个数据包的长度。发送方按照固定长度发送数据包,接收方也按照固定长度接收和解析数据包。这种方法简单易行,但可能不适用于长度变化较大的数据包。
额外字段:在数据包中添加额外的字段来表示数据包的长度。接收方先读取该字段,然后根据长度读取对应数量的数据,以此分割数据包。这种方法可以灵活处理不同长度的数据包。
使用消息协议:定义自己的消息协议(如使用特定格式的消息头),在发送和接收数据时,按照协议规定的格式进行打包和拆包。这种方法可以确保数据的完整性和正确性。
使用应用层协议:使用已有的应用层协议(如HTTP、WebSocket等)来处理数据的发送和接收。这些协议内部通常都有处理粘包问题的机制。
练习:
TCP实现字典查询
ser.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <string.h> #include <time.h> typedef struct sockaddr* (SA); typedef struct { char word[50]; char mean[256]; int ret;// 0 not find 1 find }MSG; int do_find(MSG* msg) { FILE* fp= fopen("/home/linux/dict.txt","r"); if(NULL == fp) { perror(""); exit(1); } while(1) { char buf[1024]={0}; if(NULL==fgets(buf,sizeof(buf),fp)) { break; } char *word =NULL; char * mean=NULL; word = strtok(buf," "); mean=strtok(NULL,"\r"); if(0==strcmp(word,msg->word)) { strcpy(msg->mean,mean); msg->ret = 1; } } fclose(fp); } int main(int argc, char *argv[]) { //监听套接字 int listfd = socket(AF_INET,SOCK_STREAM, 0); if(-1 == listfd) { perror("socket"); exit(1); } struct sockaddr_in ser,cli; bzero(&ser,sizeof(ser)); bzero(&cli,sizeof(cli)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); //host to net long ser.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(listfd,(SA)&ser,sizeof(ser)); if(-1 == ret) { perror("bind"); exit(1); } //同一时刻三次握手排队数 listen(listfd,3); socklen_t len = sizeof(cli); //通信套接字 int conn = accept(listfd,(SA)&cli,&len); if(-1 == conn) { perror("accept"); exit(1); } MSG msg; while(1) { bzero(&msg,sizeof(msg)); int rd_ret=recv(conn,&msg,sizeof(msg),0); if(rd_ret<=0) { break; } do_find(&msg); send(conn,&msg,sizeof(msg),0); } close(listfd); close(conn); return 0; }
clin.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <string.h> #include <time.h> #include <arpa/inet.h> typedef struct sockaddr* (SA); typedef struct { char word[50]; char mean[256]; int ret;// 0 not find 1 find }MSG; int main(int argc, char *argv[]) { int sockfd = socket(AF_INET,SOCK_STREAM,0); if(-1 == sockfd) { perror("socket"); exit(1); } struct sockaddr_in ser; bzero(&ser,sizeof(ser)); ser.sin_family = AF_INET; ser.sin_port = htons(50000); //host to net long ser.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect(sockfd,(SA)&ser,sizeof(ser)); if(-1 == ret) { perror("connect"); exit(1); } while(1) { printf("input word"); MSG msg; bzero(&msg,sizeof(msg)); fgets(msg.word,sizeof(msg.word),stdin);//zoo\n msg.word[strlen(msg.word)-1]='\0'; if(0==strcmp(msg.word,"#quit")) { break; } send(sockfd,&msg,sizeof(msg),0); bzero(&msg,sizeof(msg)); int rd_ret = recv(sockfd,&msg,sizeof(msg),0); if(rd_ret<=0) { break; } if(0 == msg.ret ) { printf("cant find,%s\n",msg.word); } else { printf("%s %s\n",msg.word,msg.mean); } } close(sockfd); return 0; }