这几天在学习Linux环境下的基础socket编程,作为一个小实验,自己编写了一个最基本简单的C/S模型,然而并没有像我想当然的那样一次性成功。一些错误来源于概念的偏差,而一些来源于对细节的忽略。总的来说,这次小小的经历对本人来说受益颇多,故此将其写成博文,做个纪念,也方便今后查阅总结。
首先,就我的理解来说一下C/S模型,不足之处还请各位多多批评。
C/S模型,或者说架构,即是Server/Client机构。其组成分为服务器端与客户端。服务器首先开启,创建socket接口,绑定并保持监听,其采取的是被动式连接,即不主动连接而等待客户端的连接请求。客户端根据服务器的网络地址和其提供的接口主动进行连接请求。服务器接受请求后通信正式开始。
下面我将介绍我所写的C/S模型的具体内容,先上代码(没有注释,抱歉 ^ w ^ )
服务器端:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define SERVER_PORT 5555
#define BUFFSIZE 2048
#define ADDR_LEN sizeof(Sockaddr_in)
typedef struct sockaddr_in Sockaddr_in;
typedef struct sockaddr Sockaddr;
int main(void){
int server_socket;
int client_socket;
Sockaddr_in server_addr;
Sockaddr_in client_addr;
char buffer[BUFFSIZE];
if((server_socket=socket(AF_INET,SOCK_STREAM,0))<0){
perror("socket");
exit(1);
}
fputs("socket created!\n",stdout);
bzero(&server_addr,ADDR_LEN);
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(server_socket,(Sockaddr *)&server_addr,ADDR_LEN)<0){
perror("connect");
exit(1);
}
fputs("socket bond!\n",stdout);
if(listen(server_socket,5)<0){
perror("listen");
exit(1);
}
fputs("listening...done!\n",stdout);
fputs("acception waiting...\n",stdout);
client_socket=-1;
int addr_len=ADDR_LEN;
while(client_socket<0)
client_socket=accept(server_socket,
(Sockaddr *)&client_addr,(socklen_t *)&addr_len);
fputs("accepted ,done!\n\n\n",stdout);
fputs("communication environment ready!\n\n",stdout);
while(1){
char test[BUFFSIZE];
recv(client_socket,buffer,BUFFSIZE,0);
if(strcmp(buffer,"out\n")==0)
break;
printf("client : ");
fputs(buffer,stdout);
putchar('\n');
printf("send > ");
fgets(buffer,BUFFSIZE,stdin);
putchar('\n');
send(client_socket,buffer,BUFFSIZE,0);
if(strcmp(test,"out\n")==0)
break;
}
fputs("communication finished!...out!\n",stdout);
close(server_socket);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#define SERVER_PORT 5555
#define BUFFSIZE 2048
#define ADDR_LEN sizeof(Sockaddr_in)
#define INET_ADDR "127.0.0.1"
typedef struct sockaddr_in Sockaddr_in;
typedef struct sockaddr Sockaddr;
int main(void){
int client_socket;
Sockaddr_in server_addr;
char buffer[BUFFSIZE];
if((client_socket=socket(AF_INET,SOCK_STREAM,0))<0){
perror("socket");
exit(1);
}
fputs("socket created!\n",stdout);
bzero(&server_addr,ADDR_LEN);
server_addr.sin_family=AF_INET;
server_addr.sin_port=htons(SERVER_PORT);
server_addr.sin_addr.s_addr=inet_addr(INET_ADDR);
fputs("connecting...\n",stdout);
if(connect(client_socket,(Sockaddr *)&server_addr,ADDR_LEN)<0){
perror("connect");
exit(1);
}
fputs("done!\n\n\n",stdout);
fputs("communication environment ready!\n\n",stdout);
while(1){
printf("send > ");
fgets(buffer,BUFFSIZE,stdin);
putchar('\n');
send(client_socket,buffer,BUFFSIZE,0);
if(strcmp(buffer,"out\n")==0)
break;
recv(client_socket,buffer,BUFFSIZE,0);
if(strcmp(buffer,"out\n")==0)
break;
printf("server : ");
fputs(buffer,stdout);
putchar('\n');
}
fputs("cmmunication finished!...out!\n",stdout);
close(client_socket);
return 0;
}
这两个程序主要实现连接后,由客户端和服务器端交互通信,任意一方发送out信息,则双方都断开连接,程序结束。
下面总结一下使用到的头文件:
- stdio.h:标准输入输出头文件,本例中主要提供 printf(),fputs(),fgets(),putchar(),perror() 这些函数的声明。
- stdlib.h:标准库头文件,本例中提供 exit() 的函数声明。
- string.h:字符创头文件,本例中提供 strcmp() 的函数声明。
- errno.h:之前以为它提供了 perror() 的函数声明,问了度娘才知道 perror() 的声明在 stdio.h 里。根据度娘的说法,该头文件 为C标准函式库里的标头档,定义了很多错误码的宏。
- unistd.h:提供 Linux 中 API 的访问功能。
- sys/types.h:提供基本系统数据类型的访问功能(具体情况还不了解,一会自己去查一下)。
- sys/socket.h:提供socket编程基本函数和一些结构体。
- arpa/inet.h:提供 htons(),htonl(),inet_addr() 和 in_addr 的访问
下面总结一下socket编程中重要的结构体:
- struct in_addr:
struct in_addr {
union {
struct {
unsigned char s_b1, s_b2, s_b3, s_b4;
}S_un_b;//An IPv4 address formatted as four unsigned chars
struct {
unsigned short s_w1, s_w2;
}S_un_w;//An IPv4 address formatted as two unsigned shorts
unsigned long S_addr;//An IPv4 address formatted as a unsigned long
}S_un;
#define s_addr S_un.S_addr
};
该结构体用来储存32位 IPv4 地址。我们可以看到,在结构体中定义了一个联合体,用户可以以四种不同的形式储存数据。用的最多的(我感觉,其实我没有用过多少次。。。)是最后一个:unsigned long S_addr,但是平时在使用的时候,我们一般使用的s_addr,这是怎么回事?看结构体最后的宏定义吧,s_addr 会被 S_un.S_addr 替换掉,最后使用的还是联合体内的定义。
该结构体我觉得比较简单,就不进一步介绍了。
- struct sockaddr_in:
struct sockaddr_in {
short int sin_family;//Address family
unsigned short int sin_port;//Port number
struct in_addr sin_addr;//Internet address
unsigned char sin_zero[8];
};
该结构体储存地址信息。短整型 sin_family 表示协议族,根据我所掌握的情况,只能用AF_INET,即TCP/IP协议族。无符号短整型 sin_port 表示端口号。下面的结构体就是我们上面讲的了,用来存储IPv4地址。最后的sin_zero[8] 只是为了使该结构体的字节大小和下面要讲的struct sockaddr相等,不用理会。
struct sockaddr 是通用的socket地址表示方法。度娘:“为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。” 但在对地址信息进行操作时,一般不用该结构体,而使用sockaddr_in,在要将其用于参数时,强制转换为 struct sockaddr 类型。由于两个结构体都为16字节,所以可以互换。
以下是struct sockaddr 的内容,由于其将端口号和IPv4地址存储在一起,不方便使用,所以才出现了sockaddr_in 以弥补它的缺陷。
- struct sockaddr:
struct sockaddr {
unsigned short sa_family;//Address family
char sa_data[14];//Protocol address
};
下面总结所使用到的一些函数:
- socket
函数原型:int socket( int af, int type, int protocol);
头文件:sys/socket.h
参数:af 为地址描述,仅 AF_INET 可用;type 为 socket 类型,可用的类型有SOCK_STREAM(TCP)、 SOCK_DGRAM(UDP)、SOCK_RAW(原始嵌套字)、SOCK_PACKET、SOCK_SEQPACKET;protocol 表示协 议,不需要时可指定为0。
返回值:无错误时,返回新套接口的描述字。若错误,返回小于0的值。
- bind
函数原型:int bind( int sockfd , const struct sockaddr *my_addr , socklen_t addrlen );
头文件:sys/socket.h
参数:sockfd 表示一个未被绑定的套接口描述字;my_addr 指向存有socket地址的地址(注意:需要强制转换类 型!);addrlen 为前面指针指向结构体的大小。
返回值:无错误则返回0。否则返回一个负值。
- listen
函数原型:int listen(int sockfd , int backlog);
头文件:sys/socket.h
参数:sockfd 表示一个捆绑而未连接的套接口的描述字;backlog 表示等待队列的最大值。
返回值:无错误则返回0,。否则返回一个负值。
- accept
函数原型:SOCKET accept( int sockfd , struct sockaddr *addr , socklen_t *addrlen);
头文件:sys/socket.h
参数:sockfd 表示一个listen()过了的套接口的描述字;addr指向接收连接实体(本例中即为sockaddr)的地址;addrlen 指向一个存有 addr 地址长度的整型数。
返回值:若无错误,返回SOCKET类型的值(即套接口描述字)。否则返回一个负值。
- connect
函数原型:int connect( int sockfd , struct sockaddr *serv_addr , int addrlen);
头文件:sys/socket.h
参数:sockfd 为套接口描述字;serv_addr 指向结构体sockaddr,包含目的端口和IP地址;addrlen 为sockaddr的长度。
返回值:若无错误则返回0。否则返回一个负值。
- send
函数原型:ssize_t send( int sockfd , const void *buf , size_t len , int flags);
头文件:sys/socket.h
参数:sockfd 为发送端套接口描述字;buf 指向待发送数据的缓冲区;len 表示实际发送数据的字符数;flags 一般置0。
返回值:若无错误则返回发送的字符数。否则返回一个负数。
- recv
函数原型:ssize_t recv( int sockfd , void *buf , size_t len , int flags );
头文件:sys/socket.h
参数:sockfd 为接收端套接口描述字;buf 指向存储缓冲区;len 表示存储字符数;flags 一般为0。
返回值:若无错误则返回实际存储的字符数。否则返回一个负数。
编程中出现的问题:
- 在server代码中误将accept函数放入接受发送的循环中,而在client中connect函数位于循环之外,导致通信发生一个来回就被断开。度娘后得知,原因是客户端为长连接,服务器端为短连接。将两者匹配即可解决问题。
- 开始使用strcmp(buffer,"out"),却发现无论双方谁输出out都不能断开。度娘无果,甚是头疼。后来突然想到,之前用的puts函数自动去掉结尾的'\n',而因为用GCC编译使用puts会产生警告,遂换为fputs函数。而fputs函数保留结尾'\n',所以将判断改为strcmp(buffer,"out\n")即可解决问题。
总的来说,此次试验收获颇多。一方面了解了C/S运作原理,学习到了socket编程的基础,还加深了对几个常用函数的认识。
通过写博文,不失为一种强迫自己将模棱两可的知识弄清楚的方法,并且有助于巩固复习。
所以,坚持。