您的位置:首页 > 其它

socket初接触---多用户服务器简单交互(下)

2016-04-23 13:36 225 查看
上一篇我们提到了很多的知识点,什么套接字,字节流,字节序….这次来简单实现一个服务器对多用户的简单消息映射交互(就是我在client端随便写段话传输给server,server接受后再返回给client端)

在写主代码之前,先看下面这个代码段:

#define ERR_EXIT(m) \
do \
{ perror(m);  \
exit(EXIT_FAILURE);\
} while(0)


反斜杠(\ , 转义字符)表示换行符无效,下一行实际上属于上一行,上面的5行实际是一行,这么写为了好看一点.

这里插个段子,在查斜杠(/)和反斜杠(\)的时候,发现了一系列对某Mi..的吐槽,(在路径表示层级上)全世界都是用/,而它是用的\,这到也不是它非要与众不同,而是因为DOS系统源自CP/M系统(微机史上开篇的一个系统),这个系统不支持目录结构(导致DOS刚刚出来的时候也不支持目录结构),而同时/被用来表示命令行参数(相当与linux中的 - 和 –),之后升级了,支持目录结构之后,再用/就会导致很多冲突,要找一个最接近的符号来表示目录层级切换,看来看去也就“\”这兄弟最适合了。

如果说要改?40多年前改还行,而这已经成了历史性的问题,当初能改的时候没改,现在再改也没什么意义了,Mi其实已经做了很多友好的设置,你可以根据自己的喜好去更改使用/。

继续看上面的代码段:

#include<stdio.h>
//用来将上一个函数发生的错误的原因输出到标准输出

void perror(const char* s);

//参数s所指的字符串会先打印出,后面再加上错误原因字符串

//这个错误原因字符串根据全局变量errno决定,当你每次调用函数出错的时候,这个函数已经更改了errno的值。

//perror这个函数总而言之就是当你调用某个函数出错时将函数的名字(char* s)和对应的errno错误信息一起输出


关于exit,终结程序,跳回操作系统。只用exit(0)代表程序的正常退出,其余的参数都表示程序异常退出。(它被定义于stdio.h中)

最后一点就是关于do {} while(0);在代码中的效果,这里while(0)保证只执行上面的程序一次,并且是先do,所以必然会执行一次判断,之后退出这个函数。在一些c函数库或者Linux内核源码中经常能看见do {} while(0); 的使用.

终于到了我们的主函数了,首先先是我们的server.c,代码见下:

void do_service(int conn)
{
char recvbuf[1024];
int n;
while(1)
{
memset(recvbuf,0,sizeof(recvbuf));

int ret = read(conn,recvbuf,sizeof(recvbuf));

if(ret == -1)
ERR_EXIT("read");
else if(ret == 0)    //ret==0表示啥都接收不到
{
printf("client close,原因可能是关闭了,也可能是连接断了\n");
break;
}
fputs(recvbuf,stdout);  //向标准输出打印读到的recvbuf

write(conn,recvbuf,sizeof(recvbuf));  //向客户端回射,说明server已经接收到你的信息了
}
}

int main(void)
{
int listenfd; //赋socket创建成功后的唯一表示符,描述字

//创建监听套接字(等待client的请求)
//通常第三个参数由前两个指定的参数决定,我们可以直接写0,表示让其自己判断
if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) <0 )
ERR_EXIT("socket");

struct sockaddr_in servaddr;   //ipv4的地址家族
//清空servaddr的内存,从指定地址servaddr的开始,用 0 来替换指定多少(第三个参数)的内存大小

memset(&servaddr , 0 , sizeof(servaddr));

servaddr.sin_family = AF_INET; //暂时我们把AF_INET和PF_INET看成一样,细微的区别

servaddr.sin_port = htons(5188);  //指定port为5188,并且将其转换为网络字节序,一个整形占2个字节,用s(hort)

servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示使用本机任意地址

int on = 1;

//解决关闭服务器立即重启时候的需要等待TIME_WAIT消失过程,除此之外,还有很多改善socket健壮性的选项,这里暂不讨论
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
ERR_EXIT("setsockopt");

//绑定地址
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("bind");

//监听套接字
if(listen(listenfd,SOMAXCONN) < 0)
ERR_EXIT("listen");

struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);

int conn;
pid_t pid;

while(1)
{
//到这一步表明已经得到了一个已连接的socket描述字,之前的listenfd到这里不再进行操作了,但是在外层可以继续接受其他client的请求。接下来我们操作conn即好
if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)
ERR_EXIT("accept");

//连接上了就打印对面的ip地址和端口,注意转换字节序,ip是4字节,port是字节
printf("ip地址是:%s \t 端口是:%d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));

//连接上了,之后需要对client作反馈操作,这里我们fork一个新的进程
pid = fork();
if(pid == -1)
ERR_EXIT("fork");
if(pid == 0) //for这个函数很特别(一次执行返回两个值,后续深入学习下这个函数),创建成功时它向子进程返回0
{
close(listenfd);    //关闭主进程的listenfd
do_service(conn);  //操作连接的conn套接字,读取数据或是回射数据,见上
exit(EXIT_SUCCESS); //跳出了上面的循环,那就意味着连接关闭了
}
else
close(conn);   //fork返回给父进程的是pid在正真系统中的唯一值,在父进程中和conn无关,直接关闭
}
return 0;
}


到这里服务端就写完了,代码中写了很多的注释,不清楚的可以仔细看看,还有一些头文件要加在开头,讲完之后在下面附上源码。

下面就开始写client.c了,代码如下:

int main()
{
//创建一个连接套接字
int sock;

if((sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
ERR_EXIT("socket");

struct sockaddr_in servaddr;

memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);   //还是需要注意转换字节序,多提醒才不会忘

servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //使用server地址。单机版没办法啦,自己连自己
//还有一种方式见下
//inet_aton("127.0.0.1",&servaddr.sin_addr);

//connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度
if(connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
ERR_EXIT("connect");

char sendbuf[1024] = {0};
char recvbuf[1024] = {0};

int n;

//从键盘上读数进来
while(fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)
{
write(sock,sendbuf,strlen(sendbuf)); //注意这里用的strlen,只发送实际上写入的数据长度
int ret = read(sock,recvbuf,sizeof(recvbuf));
if(ret == -1)
ERR_EXIT("read");
else if(ret == 0)
{
printf("client close,原因可能关闭了.\n");
break;
}
fputs(recvbuf,stdout);
//清空一次数据
memset(sendbuf,0,sizeof(sendbuf));
memset(recvbuf,0,sizeof(recvbuf));
}
close(sock); //上面那个循环都跳出来了那还sock个毛线,跳出的时候close

return 0;
}


总结:

相对于server端,client端只需要创建一个用于connect(连接)的socket,然后struct 目标server的地址(ip+port),使用connect函数进行一个连接,之后使用write函数向sock发送数据,发送完了那就是server端的事情了,这里我们可以看出来,服务器那端fputs之后,就将接收到的信息再次回射给client了,这时候client端继续read函数,接收成功的话fputs,接下爱mnemset清空数据,继续下一次while( 1 )循环。

直到直到…..break

其他情况比如exit都是重试,下一次循环

这里注意:

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口

之后将这两个.c文件编译成可执行文件,执行即可进行通信,下面是执行图:



由于文件上传不了,大家在以上代码的基础上,可以将下面的代码粘在每份文件的头部,经过测试以上代码都是能直接使用的:

#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>

#include<stdio.h>
#include<stdlib.h>

#include<arpa/inet.h>
#include<netinet/in.h>

#include<errno.h>
#include<string.h>

#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: