在对TCP服务器与客户端的工作原理进行剖析后,又进一步了解了网络编程需要的函数等,为了更深的熟悉TCP数据交互流程,于是在Linux环境下对其代码进行仿写。在编写程序前对其涉及到的一些知识进行一个了解。然后再贴代码。
一、字节序与地址结构
1.字节序分为主机字节序和网络字节序,由于主机字节序有大端和小端两种模式,不同的主机使用的模式不一定相同;而网络字节序是大端模式,所以在传输中需要将主机字节序转换为网络字节序才能传输。这将会涉及到以下函数:
#include<netinet/in.h>
uint32_t htonl(uint32_t hostlong);//将长整型的主机字节序转换为网络字节序
uint32_t ntohl(uint32_t netlong);//将长整型的网络字节序转换为主机字节序
uint16_t htons(uint16_t hostshort);//将短整型的主机字节序转换为网络字节序
uint16_t ntohs(uint16_t netshort);//将短整型的网络字节序转换为主机字节序
2.socket网络编程接口中表示socket地址的是结构体sockaddr,其通用定义如下:
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;//地址族变量
char sa_data[14];//数据
};
而对于TCP/IP协议则有着其对应的专用socket地址结构
//用于IPV4的专用地址结构:sockaddr_in
struct in_addr
{
uint32_t s_addr;
};
struct sockaddr_in
{
sa_family_t sin_family;//指定地址族
u_int16_t sin_port;//short类型的整型值,用以指定端口号
struct in_addr sin_addr;//四字节的整型值,用以保存IP地址
};
二、编程流程
了解了这些基础之后,需要再了解一下TCP服务器中重要的几个函数,在介绍函数的过程中将TCP的编程流程也一并解释:
#include<sys/socket.h>
//创建用于监听的socket套接字,创建成功返回其文件描述符,失败返回-1;
int socket(int domain,int type,int protocol);//依次设置套接字的协议簇,服务类型和协议类型
//绑定sockfd,命名创建的套接字,成功返回0,失败返回-1
ssize_t bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);//指定要绑定的文件描述符、地址结构和地址长度
//创建监听队列,用于监听TCP服务器所面向的链接,成功返回0,失败返回-1
int listen(int sockfd,int backlog);//指定被监听的套接字,以及处于完全连接状态的socket的上限
//从监听队列中获取客户端链接,成功返回socket,失败返回-1
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);//从监听队列中获取socket,指定被接受远端的socket地址,指定该socket地址的长度
//接收数据,成功返回对方的数据字节数,失败返回-1
ssize_t recv(int sockfd,void* buff,size_t len,int flags);//指定用于读取数据的socket目标,指定缓冲区的位置以及大小,flags给收发数据提供额外控制
//发送数据,成功返回发送的字节数,失败返回-1
int send(int sockfd,const void* buff,size_t len,int flags);//指定要写入数据的socket,指定写入缓冲区的位置和数据真实长度,flags提供额外控制
//与服务器进行链接,成功返回0,失败返回-1
int connect(int sockfd,const struct sockaddr *serv_addr,socklen_t addrlen);//指定socket与要连接的服务器地址和其长度
还有一些详细问题会在后续的博客或者代码中进行解释
三、简易TCP服务器的代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
//网络编程中经常需要使用的头文件
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//指定IPV4网域,字节流服务,使用默认协议版本
assert(sockfd! = -1);
//设置接收信息的socket地址结构并对相应变量进行初始化
struct sockaddr_in ser_addr;
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;//设置协议簇
ser_addr.sin_port = htons(6000);//将主机字节序转换为网络字节序,指定端口号
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//设置回环地址,防止程序运行失败
//绑定地址,此处将ser_addr强转为通用的地址结构
int res = bind(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
assert(res != -1);
//创建监听队列,并设置其上限为5,防止客户端等待时间过长
res = listen(sockfd,5);
assert(res != -1);
//由于客户端的特性,应使得其一直在接受客户端链接和处理事件,故设置成为死循环
while(1)
{
//对客户端的进行设置以及初始化
struct sockaddr_in cli_addr;
memset(&cli_addr,0,sizeof(cli_addr));
socklen_t addrlen=sizeof(cli_addr);
//建立连接,若建立失败则返回-1,建立成功则返回与其链接的文件描述符,此处也将cli_addr强转为通用地址结构
int c = accept(sockfd,(struct sockaddr*)&cli_addr,&addrlen);
if(-1 == c)
{
printf("accept error!\n");
close(c);
continue;
}//若建立失败则关闭客户端的socket然后继续下一次服务
printf("%d link success\n",c);//显示链接成功
//为接收数据设置缓冲区
char buff[128] = {0};
int n = recv(c,buff,127,0);//接收数据,一次接受127个字节,防止数据溢出,flags设置为0,表示无其他控制
if(-1 == n||0 == n)
{
printf("recv error\n");
close(c);
continue;
}//若接收错误或者未接受到则关闭客户端并进行下一次服务
printf("n=%d,buff=%s\n",n,buff);
send(c,"OK",2,0);//向客户端反馈信息
if(-1 == n||0 == n)
{
printf("send error\n");
close(c);
continue;
}//若发送错误则关闭socket且继续接受新的的客户端
close(c);//数据传输结束,关闭客户端链接
}
close(sockfd);
exit(0);
}
四、简易TCP客户端代码
服务器的代码和客户端的代码大同小异,只是由于工作性质使得客户端不再需要加上死循环,还有一点就是客户端需要使用connect()函数链接客户端,其他的基本相同
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字
assert(sockfd != -1);
//初始化要连接的服务器结构体
struct sockaddr_in ser_addr;
memset(&ser_addr,0,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(6000);
ser_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
//与客户端进行链接
int res = connect(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr));
assert(res != -1);
//从标准输入设备获取信息后将其存入buff缓冲区中
printf("please input:\n");
char buff[128] = {0};
fgets(buff,128,stdin);
//向服务器发送buff中的信息
int n = send(sockfd,buff,strlen(buff)-1,0);
if(-1 == n||0 == n)
{
printf("send error\n");
close(sockfd);
exit(0);
}//若发送失败则关闭socket并退出程序
//清空buff中的内容,并接受服务器反馈的信息
memset(buff,0,128);
n = recv(sockfd,buff,127,0);
if(-1 == n||0 == n)
{
printf("recv error\n");
close(sockfd);
exit(0);
}//若接收失败则关闭socket并退出程序
//将收到的反馈输出
printf("n=%d,buff=%s\n",n,buff);
close(sockfd);
exit(0);
//信息发送成功,反馈接受成功,关闭socket,退出程序,关闭客户端
}
五、运行测试
1.使用gcc对客户端和服务器的代码分别进行编译,编译通过。
2.先启动服务器发现阻塞,启动客户端后阻塞消失,服务器显示如下:
3.在客户端输入要发送的信息“Hello World”,服务器收到消息并反馈:
4.客户端收到反馈并结束通信,最后将TCP服务器关闭:
六、一些小问题
在整个执行过程中发生了两次阻塞,第一个是在获取链接时阻塞,分析后是因为没有客户端链接,所以在accept()方法处阻塞;第二次阻塞是因为客户端链接后无输入,在recv()方法处阻塞。还涉及了其他一些问题,代码也很简易,后续会继续完善。
新手出道,大佬请多指教。