ARM40-A5应用——采用TCP协议的C/S架构示例(2)

ARM40-A5应用——采用TCP协议的C/S架构示例(2)

2018.12.18

  本文是一个TCP通讯的示例,分为服务器和客户端两部分。
  服务器端47.98.140.167创建套接字socket,并与端口11014绑定;
   然后使套接字处于监听listen状态,调用accept等待来自客户端的连接请求;
  收到客户端的连接请求后与客户端建立连接;
  最后接收客户端发来的消息并打印出来。
  客户端创建套接字socket,然后连接connect到服务器47.98.140.167的11014端口;
  使用connect返回的连接套接字与服务器通信,交换数据。

一、模块封装

  我们将一些通用的代码封装起来,便于使用。
  tcp_net_socket.h 文件。

#ifndef __TCP__NET__SOCKET__H
#define __TCP__NET__SOCKET__H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>

extern int tcp_bind(const char* ip, int port);
extern void do_poll(int listenfd);
extern void handle_connection(struct pollfd *connfds, int num);
extern void signalhandler(void);
#endif

  tcp_net_socket.c 文件。

#include "tcp_net_socket.h"
#define OPEN_MAX 1000
#define INFTIM -1
#define MAXLINE 1024

int tcp_bind(const char* ip, int port)         //服务器端套接字的初始化与bind
{
       // create a new socket
       int listenfd;
       if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
               perror("socket");
               exit(-1);
       }

       //允许重复使用port与套接字进行绑定
       int optval = 1;
       if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (void *)&optval, sizeof(int)) == -1) {
               perror("setsockopt");
               close(listenfd);
               exit(-1);
       }

       struct sockaddr_in serv_addr;
       memset(&serv_addr, 0, sizeof(struct sockaddr_in));
       serv_addr.sin_family = AF_INET;
       serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
       serv_addr.sin_port = htons(port);

       //将socket与指定的ip、port绑定
       if(bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)) == -1) {
               perror("bind");
               close(listenfd);
               exit(-1);
       }

       return listenfd;
}

void do_poll(int listenfd)
{
       int connfd, sockfd;
       struct sockaddr_in cliaddr;
       int cliaddrlen = sizeof(struct sockaddr_in);
       struct pollfd clifds[OPEN_MAX];

       int i;
       int maxi = 0;
       int nready;

       //添加监听描述符
       clifds[0].fd = listenfd;
       clifds[0].events = POLLIN;

       //初始化客户连接描述符
       for(i = 1; i < OPEN_MAX; i++)
               clifds[i].fd = -1;

       while(1) {
               //获取可用描述符的个数
               nready = poll(clifds, maxi+1, INFTIM);         //超时时间无限长
               if(nready == -1) {
                       perror("poll error");
                       exit(1);
               }

               //测试监听描述符是否准备好
               if(clifds[0].revents & POLLIN) {
                       //接受新的连接
/*
* eg: listenfd = tcp_bind("192.168.1.130", 8888);  所以listenfd表示服务器8888的socket;
* 在服务器端,此处的 accept()用来接受参数listenfd的socket连线。参数listenfd的socket必须先经bind()、listen()函数处理过,
* 当有客户端连线connect进来时,accept()会返回一个新的socket,即connfd,去处理代码,往后的数据传送与读取就由新的socket处理,
* 而原来的参数listenfd的socket能继续使用accept()来接受新的连线请求,即等待新的客户端连接进来。
* 连线成功时,第二个参数cliaddr所指向的结构会被系统填入远程主机的地址数据。
*/
                       if((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &cliaddrlen)) == -1) {
                               if(errno == EINTR)
                                       continue;
                               else {
                                       perror("accept error");
                                       exit(1);
                               }
                       }
                       fprintf(stdout,"%s:%d connect come in\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

                       for(i = 1; i < OPEN_MAX; i++) {
                               if(clifds[i].fd < 0) {
                                       clifds[i].fd = connfd;
                                       break;
                               }
                       }

                       if(i == OPEN_MAX) {
                               fprintf(stderr,"too many clients\n");
                               exit(1);
                       }

                       //将新的描述符添加到读描述符集合中
                       clifds[i].events = POLLIN;
                       //记录客户连接套接字的个数
                       maxi = (i > maxi ? i : maxi);
                       if(--nready <= 0)
                               continue;
               }

               //处理客户连接
               handle_connection(clifds, maxi);
       }
}

void handle_connection(struct pollfd *connfds, int num)
{
       int i,n;
       char buf[MAXLINE];
       memset(buf, 0, MAXLINE);
       for (i = 1; i <= num; i++)            //只处理客户端的连接
       {
               if (connfds[i].fd < 0)
                       continue;

               //测试客户描述符是否准备好
               if (connfds[i].revents & POLLIN)
               {
                       //接收客户端发送的信息,存入buf中
                       n = read(connfds[i].fd,buf,MAXLINE);
                       if (n == 0) { //返回0,表示客户端被关闭了,例如Ctrl+C了
                               close(connfds[i].fd);
                               connfds[i].fd = -1;
                               continue;
                       }
                       //向客户端发送buf
                       write(connfds[i].fd, buf, n);
                       // printf("read msg is: ");
                       write(STDOUT_FILENO, buf, n);
               }
       }
}

void signalhandler(void) //用于信号处理,让服务器端在按下Ctrl+C或Ctrl+\时不会退出
{
       sigset_t sigSet;
       sigemptyset(&sigSet);
       sigaddset(&sigSet, SIGINT);
       sigaddset(&sigSet, SIGQUIT);
       sigaddset(&sigSet, SIGHUP);
       sigprocmask(SIG_BLOCK, &sigSet, NULL);
}

  重点是理解 void do_poll(int listenfd) 这个函数,无数据时,poll函数阻塞(超时时间无限长),两种情况下,poll 产生返回值:
  (1)有新的客户端连接时,建立一个新的connfd,代表这个新的客户端连接;
  (2)某个客户端有数据进来时,调用 handle_connection 函数处理这个客户端的数据。,
  这个例子中,handle_connection 函数收到客户端数据后,就返回相同的数据给客户端,同时在服务器的STOUT上打印出该数据。

二、服务器端的程序

#include "tcp_net_socket.h"
#define LISTENQ 12

int main(int argc, char* argv[])
{
        int listenfd, connfd, sockfd;

        if(argc < 3) {
                printf("Usage: ./servertcp ip port\n");
                exit(-1);
        }

        signalhandler();        //避免阿里云总是把这个程序给终止了
    
        /* eg: listenfd = tcp_bind("192.168.1.130", 8888); */
        listenfd = tcp_bind(argv[1], atoi(argv[2]));
        listen(listenfd, LISTENQ);
        do_poll(listenfd);

        close(listenfd);
        return 0;
}

三、客户端的程序

#include "tcp_net_socket.h"
#define MAXLINE 1024

#define max(a,b) (a > b) ? a : b
static void handle_conn(int sockfd);
static int tcp_connect(const char* ip, int port);

int main(int argc, char* argv[])
{
        if(argc < 3) {
                printf("Usage: ./clienttcp ip port\n");
                exit(-1);
        }
        /* eg: int sfd = tcp_connect("192.168.1.130", 8888); */
        int sfd = tcp_connect(argv[1], atoi(argv[2]));

        //处理连接描述符
        handle_conn(sfd);

        printf("close sfd\n");
        close(sfd);
        return 0;
}

int tcp_connect(const char* ip, int port) //用于客户端的连接
{
        int sfd;
        if((sfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { //注册新的socket
                perror("socket");
                exit(-1);
        }

        struct sockaddr_in serv_addr;
        memset(&serv_addr, 0, sizeof(struct sockaddr_in));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = inet_addr(ip);
        serv_addr.sin_port = htons(port);

        //将sfd连接至指定的服务器网络地址serv_addr
        if(connect(sfd, (struct sockaddr*)&serv_addr, sizeof(struct sockaddr_in)) == -1) {
                perror("connect");
                close(sfd);
                exit(-1);
        }

        return sfd;
}

static void handle_conn(int sockfd)
{
        char sendline[MAXLINE],recvline[MAXLINE];
        int maxfdp,stdineof;
        struct pollfd pfds[2];
        int n;

        //添加连接描述符
        pfds[0].fd = sockfd;
        pfds[0].events = POLLIN;

        //添加标准输入描述符
        pfds[1].fd = STDIN_FILENO;
        pfds[1].events = POLLIN;

        for (; ;)
        {
                poll(pfds,2,-1);
                if (pfds[0].revents & POLLIN) {
                        n = read(sockfd,recvline,MAXLINE);
                        if (n == 0) {
                                fprintf(stderr,"client: server is closed.\n");
                                close(sockfd);
                        }
                        write(STDOUT_FILENO,recvline,n);    //在标准输出打印服务器发来的内容
                }

                //测试标准输入是否准备好
                if (pfds[1].revents & POLLIN) {
                        n = read(STDIN_FILENO,sendline,MAXLINE);
                        if (n == 0) {
                                shutdown(sockfd,SHUT_WR);
                                continue;
                        }
                        write(sockfd,sendline,n);        //发给IP:port  ("192.168.1.130", 8888)
                }
        }
}

四、编译

gcc -o tcp_net_server tcp_net_server.c tcp_net_socket.c
gcc -o tcp_net_clinet tcp_net_client.c

五、运行

  首先在服务器端运行,再在客户端运行。
  服务器端如下:

./tcp_net_server 192.168.1.130 8888
192.168.1.130:55800 connect come in
1234
45667
abcd

  客户端:

./tcp_net_client 192.168.1.130 8888
1234            #客户端发的
1234            #收到服务器端返回的
45667
45667
abcd
abcd

  可见,服务器端能够收到客户端发来的数据;客户端发出数据后,能够收到服务器端返回的数据,发什么就收到什么。

六、放到阿里云服务器上运行

  首先按照参考文章《阿里云ecs禁止ping,禁止telnet》的方法,开放阿里云服务器的11014端口。
在这里插入图片描述

  然后在阿里云上编译源码:

gcc -o tcp_net_server tcp_net_server.c tcp_net_socket.c

  运行:

./tcp_net_server 47.98.140.167 11014 &                // 应为 ./tcp_net_server 0 11014 &
220.112.121.163:51172 connect come in
12346
/*
(1)要通过广域网通信的时候,局域网和局域网之间通过路由器来通信,但是当我们
使用阿里云ECS服务器的公网IP地址时,会被路由器自动的屏蔽掉,因此,我们此时
填写IP时就不能再使用原来的公网IP地址了,直接使用“0”,再输入端口号,就可以了。
(2)在 socket 程序的 "服务器监听部分" 的 "监听IP" 要设置为阿里云提供的内网IP。
(3)在 socket的客户端请求程序中请求IP必须是阿里云的公网IP 。
*/

  在192.168.1.130的客户端上运行:

./tcp_net_client 47.98.140.167 11014
12346
12346

  可见可以互相通讯。
  在ARM40上连接阿里云主机:
  首先重新编译:
arm-none-linux-gnueabi-gcc -o tcp_net_clinet_arm40 tcp_net_client.c
  然后把 tcp_net_clinet_arm40 拷贝到arm40板上运行:

./tcp_net_clinet_arm40 47.98.140.167 11014
1234
1234
432
432

  在阿里云服务器上可以收到:

112.65.48.180:13696 connect come in
1234
432

  两个客户端连接,在阿里云服务器上可以收到:

220.112.121.163:51174 connect come in
12346
112.65.48.180:13697 connect come in
4321

参考文章:

  《高质量嵌入式Linux C编程》
  《从实践中学嵌入式Linux应用程序开发》
  《UNIX环境高级编程第3版》
  IO多路复用之poll总结
  http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html
  阿里云ecs禁止ping,禁止telnet
  https://www.cnblogs.com/dadonggg/p/7885997.html

Python网络爬虫与推荐算法新闻推荐平台:网络爬虫:通过Python实现新浪新闻的爬取,可爬取新闻页面上的标题、文本、图片、视频链接(保留排版) 推荐算法:权重衰减+标签推荐+区域推荐+热点推荐.zip项目工程资源经过严格测试可直接运行成功且功能正常的情况才上传,可轻松复刻,拿到资料包后可轻松复现出一样的项目,本人系统开发经验充足(全领域),有任何使用问题欢迎随时与我联系,我会及时为您解惑,提供帮助。 【资源内容】:包含完整源码+工程文件+说明(如有)等。答辩评审平均分达到96分,放心下载使用!可轻松复现,设计报告也可借鉴此项目,该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的。 【提供帮助】:有任何使用问题欢迎随时与我联系,我会及时解答解惑,提供帮助 【附带帮助】:若还需要相关开发工具、学习资料等,我会提供帮助,提供资料,鼓励学习进步 【项目价值】:可用在相关项目设计中,皆可应用在项目、毕业设计、课程设计、期末/期中/大作业、工程实训、大创等学科竞赛比赛、初期项目立项、学习/练手等方面,可借鉴此优质项目实现复刻,设计报告也可借鉴此项目,也可基于此项目来扩展开发出更多功能 下载后请首先打开README文件(如有),项目工程可直接复现复刻,如果基础还行,也可在此程序基础上进行修改,以实现其它功能。供开源学习/技术交流/学习参考,勿用于商业用途。质量优质,放心下载使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值