知识点:
1.在网络传输中,如果使用TCP传输协议传输内容,由于TCP是以字节流的形式进行传输的,而且在TCP传输层的分节在传到IP层的时候会被拆分(如果发送的比较多的话),而且IP数据报在再往下传送的时候由于数据链路层的MTU只有1500个字节,因此IP数据报会再被分成数据帧,虽然有帧的概念,但是tcp以流的形式进行传输,而且发送的数据具有时间随机性,就是说在任何一段时间有可能发送了好几帧。接受的时候有可能接受到了很多完整的帧和半个帧,这样读的时候容易把半个帧丢弃,因此我们必须让客户端和服务器端达成一种协议,把数据的量和表示数据量的单位设定到一个结构里面。这样发送方在发送数据的时候为结构的第一个成员赋值为我要发送的数据的大小,buf里面填上我的数据,接收方在进行接受的时候就可以判断我下次该往套接字里面读多少个字节的数据。
2.其实在客户端和服务器端达成协议还有另一种办法,就是每次发送方都发送固定长度的数据,这样接收方在接受数据的时候就直接接受固定长度的数据就行了,但是这种方法最大的弊端在于,我不知道发送方要发送多少数据,我不能确定这个固定值的大小,如果设置的比较大,就会出现很少的数据占用了一个大包,导致网络利用率下降。
3.我们可以通过一个结构来描述我要发送或接受的数据格式。其中第一个成员我们用来存放即将发送或接收的个数,第二个成员我们定义一个缓冲区,用来存放我们的数据。这样客户端在发送数据的时候先发送一个4个字节的内容(有多少数据即将被发送),再发送我们的真正的数据。
我们首先来创建一个头文件,定义一些常量和结构、函数声明。
代码: myunp.h
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<stdlib.h>
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
struct packet{
int len; //用来存放即将传递的数据有多少个字节
char buf[1024]; //真正用来存放我们的发送内容的缓冲区
};
ssize_t readn(int fd, void *buf,size_t count);
ssize_t writen(int fd,const void *buf,size_t count);
代码: server02.c
#include "myunp.h" //包含我们刚刚创建的头文件
void do_chars(int connfd){ //定义服务器接收到客户端连接时,处理交互
struct packet recvbuf; //定义一个结构对象,用来接收客户端发过来的消息及消息长度。
size_t size;
ssize_t n;
while(1){
memset(&recvbuf,0,sizeof(recvbuf));
n = readn(connfd,&recvbuf.len,4); //首先接收到的是来自客户端发过来的数据的长度信息,服务器只有知道了这个值,才能得知下一次该读多少字节的数据
if(n == -1){
ERR_EXIT("readn");
}else if(n<4){ //小于4的原因是客户端在发送这个int值(4Byte)的时候,还没发完就直接关闭了客户端。
printf("client close\n");
break;
}
int ret = ntohl(recvbuf.len); //我们得把网络字节序的数据变成我们的int值
n = readn(connfd,recvbuf.buf,ret); //我们知道了该接收多少个字节就好办多了
if(n == -1)
ERR_EXIT("readn");
else if(n < ret){
printf("client close \n");
break;
}
fputs(recvbuf.buf,stdout); //我们把真实数据打印到stdout里
writen(connfd,&recvbuf,n+4); //直接回射客户端发送来的数据,由于我们已经设置好了数据的大小,也就不用再分次进行发送了。
memset(&recvbuf,0,sizeof(recvbuf));
}
}
int main(void){
int listenfd,connfd,n;
if((listenfd = socket(AF_INET,SOCK_STREAM,0))<0)
ERR_EXIT("socket");
struct sockaddr_in cliaddr,servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(45678);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
ERR_EXIT("bind");
listen(listenfd,SOMAXCONN);
socklen_t len = sizeof(cliaddr);
while(1){
if((connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&len))<0)
ERR_EXIT("Accept error!");
printf("ip = : %s , port = %d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
pid_t pid;
pid = fork();
if(pid < 0){
ERR_EXIT("fork");
}else if(pid == 0){ //如果是子进程得关闭listenfd,
close(listenfd);
do_chars(connfd);
close(connfd);
exit(EXIT_SUCCESS);
}else{ //如果是父进程,直接关闭connfd,直接继续accept。
close(connfd);
}
}
return 0;
}
代码: client02.c
#include "myunp.h" //包含我们刚才定义的头文件。
int main(void){
int sock;
if((sock = socket(AF_INET,SOCK_STREAM,0))<0)
ERR_EXIT("SOCKET");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(45678);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))<0) //connect
ERR_EXIT("CONNECT");
struct packet sendbuf; //定义一个接受数据的对象和一个发送数据的结构对象。
struct packet recvbuf;
memset(&sendbuf,0,sizeof(sendbuf));
memset(&recvbuf,0,sizeof(recvbuf));
size_t n;
while(fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL){
n = strlen(sendbuf.buf);
sendbuf.len = htonl(n);
writen(sock,&sendbuf,4+n); //我们直接发送4+n字节的数据,4是我们结构中的len的大小,n是我们发送的数据的大小。
n = readn(sock,&recvbuf.len,4); //先接受一个4字节的int值。
if(n == -1){
ERR_EXIT("readn");
}else if(n < 4){ //如果这个4字节的数据发送方没发送完就直接退出了,那么我们直接跳出循环,关闭套接字就可以了
printf("server close!!");
break;
}
int ret = ntohl(recvbuf.len); //需要把网络字节序变幻成我们熟悉的int值,并把我们的recvbuf的len值设置成这个值。
recvbuf.len = ret;
n = readn(sock,&recvbuf.buf,ret); //这样我们就知道下回该读多少个字节的数据了。
if(n == -1){
ERR_EXIT("readn");
}else if(n < ret){
printf("server close!!!");
break;
}
fputs(recvbuf.buf,stdout);
memset(&sendbuf,0,sizeof(recvbuf));
memset(&recvbuf,0,sizeof(recvbuf));
}
close(sock);
return 0;
}
代码:rw_h.c
#include "myunp.h"
ssize_t readn(int fd,void *buf,size_t count){
size_t nleft = count;
ssize_t nreadn = 0;
char * ptr = buf;
int n;
while(1){ //有时候read函数和write函数是不能一下子把套接字里面的内容读完的,因此我们得把read函数写在while循环里面。
n = read(fd,ptr,nleft);
if(n == 0){
break;
}else if(n<0){
break;
}
nleft -= n;
nreadn += n;
ptr += n;
}
return nreadn;
}
ssize_t writen(int fd,const void *buf,size_t count){
size_t nleft = count;
ssize_t nwriten = 0;
char * ptr = (char *) buf;
int n;
while(1){
n = write(fd,ptr,nleft);
if(n == 0){
break;
}else if(n<0){
break;
}
nleft -= n;
nwriten += n;
ptr += n;
}
return nwriten;
}