采用TCP协议的C/S架构示例(2)
采用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