基于TCP协议的网络程序

本篇内容将着重于写出基于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;
}         

显示结果,服务器端:

客户端:

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值