本篇内容将着重于写出基于TCP协议的服务器与客户端之间通信的代码。
TCP协议:
两台主机在基于TCP协议进行通信时,首先是要建立好连接,等到双方都确认建立好连接之后,才可以进行通信。这样做的目的则是为了保证数据通信的可靠性,但也不可否认,在建立连接过程中,需要花费时间和资源(因此基于TCP通信比基于UDP通信的速度要慢一些)。
TCP协议的特点
(1)是面向连接的,通信的速度较UDP要慢一些
(2)保证了可靠性
(3)是面向字节流的通信,所谓字节流,是指发送方发送的数据是按字节计数的,但每次发送时仍发送的是一个数据报(含有了多个字节的数据),而UDP是面向数据报的通信。
在编写实现通信的代码前,需要了解一些将要使用的接口函数
TCP的服务器端socket基本流程:socket->bind->listen->accept->send/recv->closesocket
客户端基本流程:socket->[bind->]->connect->send/recv->closesocket
1、接口函数
(1)listen函数
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:应用于客户端,使sockfd处于监听状态(处于监听状态的网卡文件才能接收到客户端发来的连接请求)。listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其他进程的请求。在TCP服务器中,listen函数将进程变为一个服务器,并指定响应的套接字变为被动连接。
1)sockfd:使用socket函数。打开套接字文件所返回的文件描述符
2)backlog:等待队列中等待处理的连接个数。
返回值:成功时返回0,失败则返回-1
注意:当系统中的资源不足以支持与客户端连接并提供服务时,就要求请求的连接处于等待队列中,等到系统中有多余的资源时,再进行连接。
为保证服务器一直处于忙碌的状态,则必须要维持一个等待队列,当服务器资源不足时,请求连接自动加入到等待队列中。一般将等待队列的长度设为5
(2)connect函数
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:客户端通过调用该函数向服务器端发起连接请求
(1)sockfd:在客户端程序中,通过调用socket函数返回的套接字文件的文件描述符
(2)addr:客户端希望连接的服务器端的存放套接字相关信息的结构体
(3)addrlen:该结构体的长度
返回值:成功时返回0,否则返回-1
当该函数调用成功时,表明TCP连接过程中通过三次握手建立连接完成。
(3)accept函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:当connect函数调用成功后,即三次握手的过程完成后,服务器端调用accept函数表示接受连接。
(1)sockfd:服务器端调用socket函数,返回该套接字文件的标识符。
(2)addr:该变量指向接收的客户端的套接字的结构体,如果将该参数置为NULL,就表示服务器端不关心客户端的地址。
(3)addrlen:上述结构体的长度
返回值:当调用该函数时,还没有客户端发来的连接请求,就阻塞等待;当连接建立成功时,服务器端成功接受连接请求后,就返回客户端网卡文件的文件描述符,否则返回-1
2、地址转换函数
我们所实现的基于TCP的网络程序,也要通过网络来进行通信,因此也需要进行ip地址的相关转换。
1.“点分十进制”转换为整型地址
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int inet_aton(const char *cp,struct in_addr *inp);
(1)cp:需要进行转换的“点分十进制”的字符串类型
(2)inp:转换后的整型地址,被封装到结构体变量中
返回值:成功转换则返回0,失败时返回-1
2.
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
该函数适于任意类型的IP地址,具体转换的类型,由参数给定
(1)af:套接字类型的地址标识,如IPv4类型的,对应于参数写AF_INET
(2)src:需转换的点分十进制 字符串类型的IP地址
(3)dst:指向转换之后的整型地址,属于输出型的参数
返回值:成功则返回0,否则则返回-1
3、整型地址转换为“点分十进制”字符串
之前在UDP函数中提到的
char *inet_ntoa(struct in_addr in);
在转换时,函数内部在静态存储区申请内存区域来存放转换后的字符串,将该区域的内存地址返回,不需要手动的释放内存区域。但是每次在调用函数时,都会将转换的结果放入该区域中,后面的结果会对前面的转换结果进行覆盖,尤其是对于多线程情况来说,多个线程都访问该内存区域,会造成结果异常的影响。
因此,需要调用一个函数(inet),可以让我们自己指定存放的内存区域,同时该函数也要适应任意类型的IP地址转换
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
其中size是dst的字节长度
返回值:成功时返回0,否则返回-1
下面则编写一个基于TCP的简单网络程序,来实现服务器端与客户端之间的通信。
基于单进程的服务器
(1)服务器端程序中调用socket函数,实现打开网卡文件,用于网络通信
(2)调用bind函数,将服务器程序与指定的IP地址和端口号进行绑定,这样客户端就可以找到该服务器与其进行连接通信
(3)该单进程的服务器是基于TCP协议的,其网卡文件要一直处于监听的状态,才能接收到客户端发来的连接请求,即调用listen函数,使网卡文件处于监听的状态
(4)当客户端调用connect函数与服务器端建立好连接之后,服务器端程序则要调用accept函数接收连接
(5)双方开始进行通信
(6)因为可能会有多个客户端需要与服务器端建立连接请求,所以服务器端则要不断地调用accept函数接收连接的请求,因此需要设置循环来不断地重复(4)-(5)
服务器端实现代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
//
int main(int argc,char* argv[])
{
//如果命令行传入的参数个数不为3,显示提示信息
if(argc != 3)
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
//打开网卡文件,将其绑定到指定的端口号和IP地址,并使其处于监听状态
int listen_sock = server_sock(argv[1],atoi(argv[2]));//调用监听函数,得到监听套接字
printf("bind and listen success,wait accept...\n");
//绑定并监听成功后,双方开始通信
struct sockaddr_in client;//定义存放客户端套接字信息的结构体
while(1)
{
socklen_t len = sizeof(client);//指定存放结构体的缓冲区的大小
//服务器端调用该函数阻塞等待客户端发来连接请求
//如果连接建立成功之后,该函数接受客户端的链接请求
int client_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(client_sock < 0)//接受失败
{
printf("accept error\n");
continue;
}
char ip_buf[24];
ip_buf[0] = 0;
//转换整型IP地址为字符串格式
inet_ntop(AF_INET,&client.sin_addr,ip_buf,sizeof(ip_buf));
//将从网络中接受的端口号转换为主机序列
int port = ntohs(client.sin_port);
printf("connect success, ip:[%s],port:[%d]\n",ip_buf,port);
//此时,双方开始互发消息进行通信
char buf[128];
while(1)
{
buf[0] = 0;//清空字符串
//服务器从客户端接受信息,如果客户端没有发来信息就阻塞等待
ssize_t s = read(client_sock,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("ip:%s,port:%d say# %s\n",ip_buf,port,buf);
}
//如果读到的为0,说明此时客户端关闭了文件描述符,与之断开了连接
//所以此时服务器应直接退出通信函数。
else if(s == 0)
{
printf("ip:%s,port:%d quit\n",ip_buf,port);
break;
}
else
{
printf("read error\n");
break;
}
//服务器端向客户端发送信息
printf("please enter#");
fflush(stdout);
buf[0] = 0;
int ss = read(0,buf,sizeof(buf) - 1);
if(ss > 0)
{
buf[ss - 1] = 0;
}
//将从键盘读到的信息写入客户端的网卡文件向其发送信息
write(client_sock,buf,strlen(buf));
printf("waiting ...\n");
}
}
return 0;
}
//得到监听套接字函数
int server_sock(char* ip,int port)
{
//打开网卡文件,得到文件描述符
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("socker error\n");
exit(1);
}
struct sockaddr_in server;
bzero(&server,sizeof(server));//使结构体server清零
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip);
server.sin_port = htons(port);
socklen_t len = sizeof(server);
// //一个服务器可能有多个网卡,一个网卡也可能绑定多个IP地址
// //INADDR_ANY可以设置在所有IP地址上进行监听,
// //直到客户端发来与指定的IP地址进行连接时,才确定使用哪个IP地址
// server.sin_addr.s_addr = htonl(INADDR_ANY);
//服务器需绑定固定的IP地址和端口号才能使客户端正确找到
if(bind(sock,(struct sockaddr*)&server,len) < 0)
{
printf("bind error\n");
exit(2);
}
//使sock处于监听状态,并且最多允许5个客户端处于连接等待状态,多于5的链接请求直接忽略
if(listen(sock,5) < 0)
{
printf("listen error\n");
exit(3);
}
return sock;//得到监听套接字
}
TCP客户端
(1)首先客户端程序先调用socket函数,打开网卡文件,得到文件描述符
(2)客户端不需要绑定固定端口号,其端口号是由内核自动分配的,所以直接调用connect函数向服务器发动连接请求
(3)当连接成功,服务器端调用accept函数接收客户端的连接请求之后,双方之间就可以开始通信了,调用通信函数,此时规定,当客户端发送“quit”字符串时,表明客户端想关闭连接,直接关闭网卡文件即可。
客户端实现代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
int main(int argc,char* argv[])
{
if(argc != 3) //当传入的参数个数不为3时,输出提示信息
{
printf("Usage:%s [ip][port]\n",argv[0]);
return 1;
}
//使用socket函数打开客户端程序的网卡文件
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
printf("socket error\n");
return 2;
}
struct sockaddr_in server; //定义结构体变量server,存放套接字的相关信息
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(argv[1]);
server.sin_port = htons(atoi(argv[2]));
socklen_t len = sizeof(server);
//使用connect函数向服务器端发送连接请求
if(connect(sock,(struct sockaddr*)&server,len) < 0)
{
printf("connect failed\n");
return 3;
}
printf("connect success\n");
//连接成功后双方开始相互通信
char buf[128];
while(1)
{
buf[0] = 0;
printf("please enter#");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s - 1] = 0;//去掉\n,如果不去掉,在与quit比较时,quit需加上\n
//当客户端发送quit时表明要与服务器端断开链接
if(strcmp(buf,"quit") == 0) //若缓冲区中的内容与“quit”一样时
{
printf("client quit\n");
break;
}
//向服务器端发送消息
write(sock,buf,strlen(buf));
printf("waiting ...\n");
}
//从服务器端接受消息
buf[0] = 0;
ssize_t ss = read(sock,buf,sizeof(buf) - 1);
if(ss > 0)
{
buf[ss] = 0;
printf("server say:%s\n",buf);
}
}
//当客户端断开连接后,关闭客户端的文件描述符
close(sock);
return 0;
}
显示结果,服务器端:
客户端: