上篇中的简单示例只能实现服务器向每一个连接他的客户端发送一个"ok",客户端在接收到信息后打印然后就会关闭,这些功能是远远不够的。
本篇将会进一步优化。
一、回显服务器
在之前的基础之上,将其改造为一个回显服务器,即客户端发送来客户端马上发回去。
设计要求如下:
1.服务器同一时间连接一个客户端
2.服务可以依次向n个客户端提供服务(假定n=5)
3.客户端接收客户发送的数据并传给服务器
4.服务器接收字符串数据并发送回客户端
5.客户输入“Q”结束本次连接
上代码:
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len,i;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2){
printf("Usage:%s <port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,θ);
if(serv_sock==-1)
error_handling("socket()error");
memset(&serv_adr,o, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind()error");
if(listen(serv_sock,5)==-1)
error_handling("listen()error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5;i++)
{
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");else
printf("Connected client %d \n",i+1);
while((str_len=read(clnt_sock, message,BUF_SIZE))1=0){
write(clnt_sock, message, str_len);
}
}
close(clnt_sock);
}
void error_handling(char *message)
{
fputs(message, stderr);fputc('\n',stderr);
exit(1);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc,char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3){
printf("Usage:%s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,θ);
if(sock==-1)
error_handling("socket()error");
memset(&serv_adr,0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect()error!");
else
puts("Connected…………………");
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n")I| !strcmp(message,"Q\n"))
break;
}
write(sock, message, strlen(message));
str_len=read(sock,message,BUF_SIZE-1);
message[str_len]=0;
printf("Message from server:%s", message);
}
close(sock);
return θ;
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(0);
}
上述的echo服务器-客户端,实现了客户端发一条数据服务器端马上就返回。但在写的过程我百思不得其解,为什么服务器端可以收一句就发一句而不会把上一条数据也发回去,他是怎么确定数据边界的呢(因为我知道TCP是不存在数据边界的,所以这个是怎么实现的)?去问了gpt,哦原来他并没有处理只是简单的收到就发,新的数据会覆盖原来的数据,缓冲区只有当前数据。
所以问题就来了,如果数据太大分了两个包发过去,但是客户端还没等发完就接受了,那是不是数据就要丢失了?
坚决不允许!
二、完美echo!
在开始动手写基于TCP网络的网络编程的时候才是真正开始TCP协议的时候。笨蛋如我都能想到要对连续消息分割,那么应用层一般采用什么手段呢?
应用层通常使用以下一种或多种机制来识别和分割消息:
固定消息长度: 在应用层协议中,规定每条消息的长度是固定的。接收端按照这个固定长度来分割消息。这种方法适用于消息长度固定的情况,例如传输固定大小的数据块。
消息长度字段: 在每条消息的开头包含一个指示消息长度的字段。接收端首先读取这个长度字段,然后根据长度字段指示的大小来接收完整的消息。这种方法适用于变长消息的情况。
特定分隔符: 在消息之间使用特定的分隔符(如换行符
\n
、回车符\r
或自定义分隔符)来标记消息的结束。接收端根据分隔符来分割消息。这种方法适用于消息之间没有固定长度的情况,但有明确的分隔符。消息头: 在每条消息的开头包含一个固定长度的消息头,消息头中包含有关消息的元数据,如消息类型或长度信息。接收端首先解析消息头以确定消息的长度或其他信息,然后根据这些信息来接收消息。
时间限制: 在某些情况下,应用程序可能约定在没有更多数据到达的一段时间后认为消息已经接收完毕。这可以通过设置一个时间限制来实现,但这种方法可能不够可靠。
复杂的应用层协议: 在一些复杂的应用中,可能需要使用更复杂的协议来识别和分割消息。这些协议可以包括消息头、消息体、校验和等内容,以确保消息的完整性和正确性。
所以接下来我们就来解决我们的echo客户端.因为在接收数据之前可以先接收消息大小,所以可以根据收发数据大小是否一致来判断数据传送是否都正常.
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len,i;
struct sockaddr_in serv_adr;
if(argc!=3){
printf("Usage :%s <IP><port>\n",argv[0]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,θ);
if(sock==-1)
error_handling("socket()error");
memset(&serv_adr,θ, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("connect()error!");
else
puts("Connected...........");
//这里有改变
while(1)
{
fputs("Input message(Q to quit):",stdout);
fgets(message,BUF_SIZE,stdin);
if(!strcmp(message,"q\n")I|!strcmp(message,"Q\n"))
break;
str_len=write(sock, message, strlen(message));//写入大小
}
recv_len=0;//接收大小
while(recv_len<str_len)
{
recv_cnt=read(sock,&message[recv_len],BUF_SIZE-1);//实际读的大小
if(recv_cnt==-1)
error_handling("read()error!");
recv_len+=recv_cnt;
}
message[recv_len]=0;
printf("Message from server:%s", message);
}
close(sock);
return θ;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
while(recv_len<str_len) 当你看到这个你可能会觉得你不说是判断一致吗?为啥不用recv_len!=str_len,诚然!=更直观,但是我要说了 :
写这个循环是因为:当对方发的太多了一次读不完 所以我们判断发送的大小和实际接收的大小是否一致,如果小的话就一直读,但是万一读多了 那么如果还用!=的话就死循环了,不可取!
但其实一般情况下两端是不知道对方要发多少数据的,就像我问gpt问题他怎么知道我今天又要问什么愚蠢的问题呢!但是TCP只负责传数据不管那么多所以就要设计合理的应用层协议来控制数据边界!
这里给出一个《TCP/IP 网络编程》尹圣雨 中的一个示例,体会为什么么说设计应用层控制数据边界。
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char opmsg[BUF_SIZE];
int result, opnd_cnt,i;
struct sockaddr_in serv_adr;
if(argc!=3)
{
printf("Usage:%s <IP><port>\n",argv[θ]);
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,θ);
if(sock==-1)
error_handling("socket()error");
memset(&serv_adr,θ, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect()error!");
else
puts("Connected..........");
fputs("Operand count:",stdout);
scanf("%d",&opnd_cnt);
opmsg[0]=(char)opnd_cnt;
for(i=0; i<opnd_cnt;i++)
{
printf("Operand %d:",i+1);
scanf("%d",(int*)&opmsg[i*0PSZ+1]);
}
fgetc(stdin);
fputs("operator:", stdout);
scanf("%c",&opmsg[opnd_cnt*OPSZ+1]);
write(sock, opmsg, opnd_cnt*OPSZ+2);
read(sock,&result,RLT_SIZE);//TCP不存在边界
}
printf("operation result:%d \n", result);
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4
void error_handling(char *message);
int calculate(int opnum, int opnds[],char oprator);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char opinfo[BUF_SIZE];
int result, opnd_cnt,i;
int recv_cnt, recv_len;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2){
printf("Usage :%s <port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,θ);
if(serv_sock==-1)
error_handling("socket()error");
memset(&serv_adr,o, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr))==-1)
error_handling("bind()error");
if(listen(serv_sock,5)==-1)error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5;i++)
{
opnd_cnt=0;
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&clnt_adr_sz);
read(clnt_sock,&opnd_cnt,1);
}
recv_len=0;
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt=read(clnt_sock,&opinfo[recv_len],BUF_SIZE-1);
recv_len+=recv_cnt;
}
result=calculate(opnd_cnt,(int*)opinfo, opinfo[recv_len-1]);
write(clnt_sock,(char*)&result, sizeof(result));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
int calculate(int opnum, int opnds[],char op)
{
int result=opnds[θ],i;
switch(op)
{
case'+':
for(i=1; i<opnum; i++)result+=opnds[i];
break;
case'-':
for(i=1; i<opnum; i++) result-=opnds[i];
break;
case'*':
for(i=1; i<opnum; i++) result*=opnds[i];
break;
}
return result;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}