找回密码
 立即注册
Qt开源社区 门户 查看内容

Linux测试TCP通信

2019-9-30 08:31| 发布者: admin| 查看: 1140| 评论: 0

摘要: 传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。百度百科在TCP/IP中,TCP和UDP是最主要的传输层协议,也是应用希望进行网络通信时 ...

传输控制协议(TCP,Transmission Control Protocol)是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
百度百科


在TCP/IP中,TCP和UDP是最主要的传输层协议,也是应用希望进行网络通信时直接接触的协议。现在就来尝试一下在Linux上实现TCP-Server和TCP-Client的双向通信。

系统环境:Ubuntu 18.04/vim/g++/make

本文假设读者有一定C++基础,基本了解TCP协议。

要先实现在Linux的C++程序运行,首先要在Linux系统上安装C++编译环境,这里使用vim编写,g++编译,makefile构建。

TCP/IP协议是现代网络通信的核心协议,在网络通信时,由于各种各样的原因,如交换设备掉电、网络堵塞、主机宕机等,都可能造成数据传输的失败。这时候保证数据能够完整的到达目的地就尤为重要。这时候就需要TCP协议的握手机制来实现可靠的通信。

所谓握手机制就是在正式开始数据传输之前进行一些传输参数的协商,建立连接后再传送数据就可以对每一组数据进行确认,以保证可靠传输。

TCP协议提供了端口号的概念,每台主机共有65536个端口分别编号0~65535,。每个端口对应一个进程,如最常见的80代表HTTP连接,21代表FTP服务,22代表SSH服务,3389 代表Windows远程登录等等。这样就可以通过不同的端口号来识别不同进程。在实际使用时,要注意避免使用1~1023之间的端口号,因为这部分端口号已经被系统占用,用户可使用的端口号应尽量选择在1024~65535之间。

还有一个重要概念是套接字(socket),这是一种将应用与端口连接起来的软件实现,在C++中提供了大量的类库实现TCP功能,我们只需要使用合适的类即可。使用socket进行的通信统称为socket通信,下面是基于socket的TCP网络服务端和客户端实现。

TCP-Server:

在TCP通信中,服务端会建立端口监听,等待客户端的连接请求。当服务端接收到连接请求后进行握手,再通过相应函数进行数据的传输。

在服务器准备监听之前,需要首先创建一个socket来监听端口,在 sys/socket.h 头文件中有与socket相关的函数。建立socket的函数原型如下:
int socket(int af, int type, int protocol);
其中,af为地址族(Address Family),也就是IP地址类型,常见的有AF_INET(即IPv4)和AF_INET6(即IPv6),此时需要了解一个常用地址 127.0.0.1 ,该地址代表本机(localhost),在本次通信实验中我们就在一台主机上同时建立服务端和客户端。

type为连接类型,主要有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据包套接字/无连接的套接字) ,在本次实验中由于是TCP协议,需要建立连接,所以使用 SOCK_STREAM 进行。

protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。我们使用 IPPROTO_TCP  。

可能有同学有个疑问,这个函数的返回值究竟是个什么,为什么是int类型,其实这个返回值是一个文件描述符,如果成功就返回相应的描述符,如果不成功就返回 INVALID_SOCKET ,这里 INVALID_SOCKET 的值为-1。为了后续调用套接字,我们需要一个变量来承载这个描述符。即:
int listenfd = -1;
listenfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字

然后对于这个新创建的套接字,我们需要将一个本地协议地址赋予这个套接字,这时的函数原型如下:
int PASCAL FAR bind (SOCKET s, const struct sockaddr FAR *addr, int namelen);
其中,s为刚刚创建的套接字的文件描述符。addr是本地协议地址的结构体 sockaddr 的指针,这个结构体原型在netinet/in.h文件中声明。addrlen是该地址结构的长度。对于TCP,调用bind函数可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

这时我们就需要先创建一个本地地址协议的结构体,在实际使用中,往往使用sockaddr_in结构体而不是sockaddr,相比 sockaddr , sockaddr _in将 sockaddr 中的一个字符串拆成了多个成员,更容易使用,而且与 sockaddr 是并列关系,指向 sockaddr 的指针可以直接指向 sockaddr _in而不会出错。sockaddr _in的创建如下:
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(5678);

在上述的servaddr中,sin_family 为协议族,AF_INET代表使用了IPv4协议、sin_addr为地址结构体,htonl(INADDR_ANY)为任意地址,sin_port 为端口,htons(5678)监听5678端口。随后使用bind函数将地址赋予套接字如下:
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
接着我们就可以使服务端开始监听端口了,使用listen函数原型如下:
int listen(int fd, int backlog);
其中,fd为套接字,backlog比较难以理解,简单来说,他就是在TCP连接中已经申请连接正在握手的套接字(处在未完成连接队列)的数量和已经完成握手处在连接状态的套接字(处在已完成连接队列)的数量之和的最大值。我们在测试中只有一个客户端连接,所以取小一点,取10,其实1都可以。如下:
listen(listenfd, 10);
这时服务器就已经在监听了,为了让服务器一直处理连接和接收数据,我们在程序中使用一个死循环进行处理,其中使用accept函数等待套接字建立连接,函数原型如下:
SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
其中sockfd为等待连接的套接字。addr是一个可选参数,为sockaddr指针指向一缓冲区,其中接收为通讯层所知的连接实体的地址。addrlen与 addr 一起使用,指向存有addr地址长度的整型数。返回值为一个新的套接字,该套接字可以认为是和客户端连接的套接字,包含客户端的IP地址和端口。在后面等待数据时使用的就是这个套接字了。

建立连接后获得了客户端的新的套接字,就可以准备接受数据了,接受数据使用recv函数,原型如下:
int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);
其中,s为待接收数据的套接字,buf为存放数据的缓冲区地址,len为缓冲区长度,flags为特殊操作的标志位,一般写0即可。返回值为数据字节数。接收到数据后直接传回客户端并打印到屏幕。发送数据使用send函数,原型如下:
int send( SOCKET s,const char* buf,int len,int flags);
参数与recv一致,只是接受变为发送而已,len代表实际发送的字节数。最后代码如下:
while (1)
{
connfd = accept(listenfd, (struct sockaddr *)NULL, NULL);
n = recv(connfd, buff, MAXLINE, 0);
send(connfd, buff, n, 0);
buff[n] = '\0';
printf("%s\n", buff);
close(connfd);
}

服务器端完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define MAXLINE 4096 //缓冲区最大长度

int main(int argc, char **argv)
{
int listenfd, connfd;
int n;
struct sockaddr_in servaddr;
char buff[MAXLINE];

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(5678);

listenfd = socket(AF_INET, SOCK_STREAM, 0); //创建套接字
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); //连接至端口

listen(listenfd, 10); //开始监听

printf("Waiting Connection\n");
while (1)
{
connfd = accept(listenfd, (struct sockaddr *)NULL, NULL); //等待连接
n = recv(connfd, buff, MAXLINE, 0); //接收数据
send(connfd, buff, n, 0); //发送数据
buff[n] = '\0';
printf("%s\n", buff);
close(connfd); //断开连接
}
close(listenfd); //关闭监听
return 0;
}

TCP-Client:

对于客户端,我们需要指定连接的对象,也就是服务器端的IP地址和端口,在代码中我们将服务器地址作为程序参数传入。方法为使用主函数参数,主函数完整函数头如下:
int main(int argc, char **argv);
其中argc为参数个数,argv为参数字符串数组。在参数中,第一个参数为文件名被占用,第二个参数开始为自定义参数,所以当我们需要加入一个IP地址字符串作为参数时,实际上argc的值为2,如下代码判断参数是否合法:
if (argc != 2) //判断参数是否合法
{
printf("usage: ./client <ipaddress>\n");
return 0;
}

随后创建套接字,创建套接字与服务端相同。与服务端不同的是,客户端不需要监听某一个端口,所以不需要将套接字与端口相连。但需要将程序参数中的IP地址处理为能够使用的格式,通过 inet_pton 函数将地址字符串转换为适用于网络传输的数值格式并存入 sockaddr_in 结构体。inet_pton 函数原型如下:
int inet_pton(int family, const char *strptr, void *addrptr);
其中family指定协议族,strptr为字符串形式的IP地址,addrptr为存入的结构体地址成员的指针。实际代码如下:
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5678);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

随后使用套接字连接服务端,连接服务端使用的函数原型如下:
int connect(SOCKET s, const struct sockaddr * name, int namelen);
其中,s为创建连接使用的套接字,name为sockaddr_in结构体的指针,其中包含想要连接的主机地址和端口号,namelen为sockaddr_in的结构长度。如下:
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
这时客户端就已经与服务端建立连接了,下面就要从键盘获取一串字符并发送给服务端。发送与服务端相同,使用send函数,实际上,无论对于服务端还是客户端,都可以使用send和recv函数通过已经创建连接的套接字发送或接收数据。这里发送和接收如下:
fgets(sendline, MAXLINE, stdin);
send(sockfd, sendline, strlen(sendline), 0);
n = recv(sockfd, recvline, MAXLINE, 0);
recvline[n] = '\0';
printf("%s\n", recvline);

最终完整代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define MAXLINE 4096

int main(int argc, char **argv)
{
int sockfd, n;
struct sockaddr_in servaddr;
char sendline[MAXLINE], recvline[MAXLINE];

if (argc != 2) //判断参数是否合法
{
printf("usage: ./client <ipaddress>\n");
return 0;
}

sockfd = socket(AF_INET, SOCK_STREAM, 0);

servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5678);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

printf("send msg to server: \n");
fgets(sendline, MAXLINE, stdin); //从键盘获取字符串
send(sockfd, sendline, strlen(sendline), 0); //发送字符串
n = recv(sockfd, recvline, MAXLINE, 0); //接收字符串
recvline[n] = '\0'; //末尾加'\0'
printf("%s\n", recvline);
close(sockfd);
return 0;
}

测试:



在Ubuntu中使用vim编辑代码


在完成代码之后将程序进行编译,编译使用g++命令直接编译,服务端代码保存为server.cpp,客户端代码保存为client.cpp。运行如下命令:
g++ -g -o server server.cpp
g++ -g -o server client.cpp

然后再Ubuntu中再开启一个终端,两个终端分别运行以下两行指令:
./server #启动服务端监听
./client 127.0.0.1 #启动客户端连接至本地服务器

在运行客户端的终端上会等待输入要传输的字符串,随便输入一串回车,即可看到服务端出现了相同的字符串并且又回传给了客户端。



连接测试成功


测试成功。

通过对服务端死循环中代码和客户端发送及接收部分代码的处理,可以实现连续收发,不再赘述。本文应该可以让有一定C语言水平但尚未接触过网络编程的同学做出能够实现网络通信功能的程序。

All done!



更多精彩内容,点击阅读原文查看我的博客~

----------------------------------------------------------------------------------------------------------------------
我们尊重原创,也注重分享,文章来源于微信公众号:TechJH,建议关注公众号查看原文。如若侵权请联系qter@qter.org。
----------------------------------------------------------------------------------------------------------------------

鲜花

握手

雷人

路过

鸡蛋

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了