实验目的:完成“网络编程技术”参考书上 “2.12 SOCKET应用实例”中的两个编程实例,并在ubuntu与树莓派之间进行验证。
实验环境:Windows10,Ubuntu18.04,树莓派
参考资料:《网络编程技术》
一、实验相关概念
(一)流式套接字
数据可以在客户端按顺序发送,并且到达服务器的顺序也是一致的。例如,客户端依次发送1、2,服务器接收到的也是1、2,流式套接字是可靠的、面向连接的。
(二)非阻塞模式
当线程从某通道进行读写数据时,若没有数据可用时,该线程可以执行其他任务。线程通常将非阻塞IO的空闲时间用于在其通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。因此,非阻塞模式可以让服务器使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
二、步骤
0.先前配置
- Ubuntu需要使用桥接模式和树莓派连接到同一网络
- 可以使用VNC或者putty连接到树莓派
- 参考
1.面向连接的流式套接字C/S例子
这里随便谁为服务器或者客户端都可以,为了方便操作。树莓派为服务器,Ubuntu为客户端。
运行图
1)服务器端代码
在Ubuntu上写入并编译
vim server.c
gcc server.c -o server
/*server.c -- 一个socket流的服务器端代码*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "9090" //接入端口
#define BACKLOG 10 //可容纳多少个接队列
void sigchld_handler(int s)
{
//暂时停止当前进程的执行,直到信号到达或子进程终止。
while (waitpid(-1, NULL, WNOHANG) > 0);
}
//得到IPv4或者IPv6地址
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
int sockfd, new_fd; //监听sock_fd,new_fd新连接
struct addrinfo hints, *servinfo, *p;
struct sockaddr_storage their_addr; //接入者信息
//数据类型“socklen_t”和int应该具有相同的长度。否则,您将破坏BSD套接字层的填充。
socklen_t sin_size;
//Sigaction函数可用于查询或设置信号处理
struct sigaction sa;
int yes = 1;
char s[INET6_ADDRSTRLEN];
int rv;
//将Hints置0
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE; //用自己的IP
if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n", gai_strerror(rv));
return 1;
}
//遍历所有结果并链接到第一个成功连接的套接
for (p = servinfo; p != NULL; p = p->ai_next)
{
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("server:socket");
continue;
}
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
{
perror("setsockopt");
exit(1);
}
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("server:bind");
continue;
}
break;
}
//如果指针P为null,显示一条错误信息
if (p == NULL)
{
fprintf(stderr, "server:failed to bind\n");
return 2;
}
//所有的工作都放在这个结构里
freeaddrinfo(servinfo);
/*
使套接字处于侦听传入连接请求的状态。如果监听失败,则退出。
*/
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}
sa.sa_handler = sigchld_handler; //reap all dead processes
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1)
{
perror("sigaction");
exit(1);
}
printf("server:waiting for connections...\n");
//主要的accept()循环
while (1)
{
sin_size = sizeof their_addr;
new_fd = accept(sockfd, (struct sockaddr*)&their_addr, &sin_size);
if (new_fd == -1)
{
perror("accept");
continue;
}
inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr*)&their_addr), s, sizeof s);
printf("server:got connection from %s\n", s);
if (!fork()) //这是子进程
{
close(sockfd); //子进程不需要监听
if (send(new_fd, "Hello,world!", 13, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); //父进程不需要这个
}
return 0;
}
2)客户器端代码
在树莓派上写入并编译
vim client.c
gcc client.c -o client
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT "9090" //连接端口号
#define MAXDATASIZE 100 //一次性的最大字节数
//得到IPv4或者IPv6
void *get_in_addr(struct sockaddr *sa)
{
if (sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct addrinfo hints, *servinfo, *p;
int rv;
char s[INET6_ADDRSTRLEN];
//如果命令行参数不等于 2 ,则执行下面的语句
if (argc != 2)
{
fprintf(stderr, "usage:client hostname\n"); //打印错误消息
exit(1); //退出
}
//将hints内存的内容置 0
memset(&hints, 0, sizeof hints);
//设置协议无关
hints.ai_family = AF_UNSPEC;
//设置套接为流
hints.ai_socktype = SOCK_STREAM;
if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n", gai_strerror(rv));
return 1;
}
//遍历所有返回结果并链接到第一个成功连接的套接
for (p = servinfo; p != NULL; p = p->ai_next)
{
//创建一个套接字
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("client:socket");
continue;
}
//连接状态判断
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("client:connect");
continue;
}
//如果创建套接字成功且连接成功,则退出循环
break;
}
//如果套接口地址为空,则打印结果
if (p == NULL)
{
fprintf(stderr, "client:failed to connect\n");
return 2;
}
//inet_ntop 函数可以将 IP 地址在“点分十进制”和“整数”之间转换
inet_ntop(p->ai_family, get_in_addr((struct sockaddr*)p->ai_addr), s, sizeof s);
printf("client:connecting to %s\n", s);
//freeaddrinfo 函数释放 getaddriinfo 函数返回的存储空间
freeaddrinfo(servinfo);
//recv 函数用于判断缓冲区数据传输的状态,传输异常则打印消息比并退出
if ((numbytes = recv(sockfd, buf, MAXDATASIZE - 1, 0)) == -1)
{
perror("recv");
exit(1);
}
//将字符数组的最后一位置 \0 ,用于后面一次性输出
buf[numbytes] = '\0';
printf("client:received %s\n", buf);
close(sockfd);
return 0;
}
2.一个非阻塞的多人聊天服务器例子
为了方便操作,树莓派为服务器,Ubuntu为客户端。
先运行服务器,再开启客户端,客户端可以用两个Ubuntu来模拟。
服务器端可见
客户端可见
1)服务器端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#define PORT "9090" //监听的端口
//得到IPv4或者IPv6
//sockaddr 结构体:存储参与(IP)Windows/linux套接字通信的计算机上的一个internet协议(IP)地址
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET){
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
int main(void)
{
fd_set master; //主文件描述符列表
fd_set read_fds; //select() 的临时文件描述符列表
/*
当调用 select() 时,由内核根据 IO 状态修改 fd_set 的内容
由此来通知执行了 select() 的进程哪一 socket 或文件发生了可读或可写事件
*/
int fdmax; //最大文件描述符
int listener; //监听套接字描述符
int newfd; //新接受的套接字描述符
//sockaddr_storage 结构体:存储套接字地址信息
struct sockaddr_storage remoteaddr; //客户端地址
socklen_t addrlen;//socklen_t 和 int 相同长度的一种类型
char buf[256]; //用于客户端数据的缓冲区
int nbytes;
int yes=1; //for setsockopt() SO_REUSEADDR,below
int i,j,rv;
char remoteIP[INET_ADDRSTRLEN];
struct addrinfo hints,*ai,*p; //地址信息结构体
FD_ZERO(&master); //清除主文件描述符列表
FD_ZERO(&read_fds); //清楚临时文件描述符列表
//给我们一个套接字并绑定它
memset(&hints, 0, sizeof hints); //地址信息置零
hints.ai_family = AF_UNSPEC; //AF_UNSPEC(协议无关)
hints.ai_socktype = SOCK_STREAM; //SOCK_STREAM(流)
hints.ai_flags = AI_PASSIVE; //AI_PASSIVE(被动的,用于 bind)
if((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0)
{
fprintf(stderr, "selectserver:%s\n", gai_strerror(rv));
exit(1);
}
//遍历所有的结果
for(p = ai; p != NULL; p = p->ai_next)
{
//创建套接字并赋值给 listener 套接字描述符
listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if(listener < 0)
{
continue;
}
//setsockope 函数用于任意类型、任意状态套接字的设置选项
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
/*
bind 函数:用于未连接的数据报或流类套接口,把一本地址与一套接口捆绑
在 connect() 或 listen() 调用前使用
*/
if(bind(listener, p->ai_addr, p->ai_addrlen) < 0)
{
close(listener);
continue;
}
break;
}
if(p == NULL)
{
fprintf(stderr, "selectserver:failed to bind\n");
exit(2);
}
//到了这里,意味着服务器的地址和端口绑定完成,可以释放 ai 的内存
freeaddrinfo(ai);
/*
listen 函数:创建一个套接口并监听申请的连接.
参数一:用于标识一个已捆绑未连接套接口的描述符
参数二:等待连接队列的最大长度(这里是 10)
*/
if(listen(listener, 10) == -1)
{
perror("listen");
exit(3);
}
//将侦听器添加到主文件
FD_SET(listener, &master);
//跟踪最大的文件描述符
fdmax = listener; //到目前为止,是这个
//主循环
for(;;)
{
read_fds = master; //将主文件描述符表复制到临时文件描述符表
/*
select 函数:确定一个或多个套接口的状态,如需要则等待。
原型:int select( int nfds, fd_set FAR* readfds, fd_set * writefds,
fd_set * exceptfds, const struct timeval * timeout);
nfds:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,
不能错!在Windows中这个参数的值无所谓,可以设置不正确。
readfds:(可选)指针,指向一组等待可读性检查的套接口。
writefds:(可选)指针,指向一组等待可写性检查的套接口。
exceptfds:(可选)指针,指向一组等待错误检查的套接口。
timeout:select()最多等待时间,对阻塞操作则为 NULL。
*/
if(select(fdmax + 1, &read_fds, NULL, NULL, NULL) == -1)
{
perror("select");
exit(4);
}
//运行现有的连接,查找要读取的数据
for(i = 0; i <= fdmax; i++)
{
/*
宏原型:int FD_ISSET(int fd,fd_set *fdset)
在调用 selelct() 函数后,用 FD_ISSET 来检测 fd 在 fdset 集合中的状态是否变化
返回整形,当检测到 fd 状态发生变化时返回真,否则返回假
*/
if(FD_ISSET(i, &read_fds)) //得到了一个连接
{
if(i == listener)//如果新连接为最大文件描述符
{
//处理新连接
addrlen = sizeof remoteaddr;
newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen);
if(newfd == -1)
{
perror("accept");
}
else
{
FD_SET(newfd, &master); //添加到主文件描述符列表
if(newfd > fdmax) //记录最大值
{
fdmax = newfd;
}
printf("selectserver:new connection from %s on socket %d\n", inet_ntop(remoteaddr.ss_family, get_in_addr((struct sockaddr*)&remoteaddr), remoteIP, INET_ADDRSTRLEN), newfd);
}
}
else
{
//处理来自客户端的数据
if((nbytes = recv(i, buf, sizeof buf, 0)) <= 0)
{
//出现错误或连接被客户端关闭
if(nbytes == 0)
{
//连接关闭了
printf("selectserver:socket %d hung up\n", i);
}
else
{
perror("recv");
}
close(i); //关闭
FD_CLR(i, &master); //从主文件描述符列表中删除
}
else
{
//我们从一个客户那里得到了一些数据
for(j =0; j <= fdmax; j++)
{
//发给大家!
if(FD_ISSET(j, &master))
{
//除了监听器和我们自己
if(j != listener && j != i)
{
if(send(j, buf, nbytes, 0) == -1)
{
perror("send");
}
}
}
}
}
} //END handle from client
} //END got new incoming connection
} //END looping through file descriptors
} //END for(;;)--and you thought it would never end!
return 0;
}
2)客户端代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT "9090" //客户端连接的端口
#define MAXDATASIZE 100 //一次性得到的最大的字节数
int sockfd, numbytes;
char buf[MAXDATASIZE];
//得到IPv4或者IPv6
void *get_in_addr(struct sockaddr *sa)
{
if(sa->sa_family == AF_INET)
{
return &(((struct sockaddr_in*)sa)->sin_addr);
}
return &(((struct sockaddr_in6*)sa)->sin6_addr);
}
//和server一样
void *recvMag()
{
while(1)
{
if((numbytes = recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1)
{
perror("recv");
exit(1);
}
if(numbytes == 1)
continue;
buf[numbytes] = '\0';
printf("\nreceived:%s\n",buf);
}
}
int main(int argc, char *argv[])
{
struct addrinfo hints, *servinfo, *p;
int rv;
char s[INET6_ADDRSTRLEN];
pthread_t t1;
char mag[MAXDATASIZE];
if(argc != 2)
{
fprintf(stderr, "usage:client hostname\n");
exit(1);
}
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
fprintf(stderr, "getaddrinfo:%s\n",gai_strerror(rv));
return 1;
}
for(p = servinfo; p != NULL; p = p->ai_next)
{
if((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
perror("client:socket");
continue;
}
if(connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
close(sockfd);
perror("client:connect");
continue;
}
break;
}
if(p == NULL)
{
fprintf(stderr, "client:failed to connect\n");
return 2;
}
inet_ntop(p->ai_family, get_in_addr((struct sockaddr*)p->ai_addr), s, sizeof s);
printf("client:connecting to %s\n",s);
freeaddrinfo(servinfo);
int err = pthread_create(&t1, NULL, recvMag, NULL);
if(err != 0)
{
printf("receive failed");
exit(1);
}
while(1)
{
scanf("%s", mag);
if(send(sockfd, mag, sizeof mag, 0) == -1)
{
printf("send failed!\n");
}
}
//关闭(sockfd);
return 0;
}
完成后可以用Ctrl+C中断程序
三、其它
Socket应用的聊天程序网上还有很多,如果有兴趣,可以自己找来学习。
网络通信编程11and12_2
上一篇:通过Teardrop学习网络通信编程
下一篇:(敬请期待)