网络编程---Linux环境下TCP服务器与客户端代码仿写

在对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()方法处阻塞。还涉及了其他一些问题,代码也很简易,后续会继续完善。

新手出道,大佬请多指教。

猜你喜欢

转载自blog.csdn.net/qq_45132647/article/details/104633662