先简单了解一下socket是什么,有什么作用,以及怎样使用,先以编写服务器为例
在进行服务器的编写之前我们先了解一般服务器编写的步骤:
1:创建套接字
2:对套接字,ip,以及服务器地址进行绑定
3:监听套接字
4:接收客户端连接
5:与客户端的数据交互
6:关闭套接字
socket又叫做套接字,用于TCP/IP,IPX中进行通信,是一种网络编程接口,即是一种文件描述符,主要用于会话层与传输层之间的通信。
1,创建套接字
socke也是一个函数,该函数的功能就是返回一个套接字,一般为3,因为0,1,2这三个文件描述符已被系统占用,我们使用man手册看一下需要哪一些参数和具体的返回值
int socket(int domain, int type, int protocol); //函数原型
int sfd =socket(AF_INET,SOCK_STREAM,0); //使用方法
if(sfd==-1)
{
perror("socket failed\n");
exit(-1);
}
以上代码是socket函数的具体使用,分别解释一下函数原型中的参数意义:
domain : 如果使用的是IPV4协议第一个参数为AF_INET,如果使用的是IPV6协议就使用 AF_INET6,这里使用IPV4。
type: 如果是用于TPC协议的编写就使用SOCK_STREAM,如果用于UDP的编写使用SOCK_DGRAM,这里使用TCP。
protocol : 只需要一个协议族使使用0。
正确返回套接字,错误返回-1;
2,绑定套接字
绑定套接字使用bind函数,看一下具体函数原型和使用方法
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //函数原型
struct sockaddr_in addr; //具体参数和函数使用
addr.sin_family=AF_INET;
addr.sin_port=ntohs(9527);
addr. sin_add.s_addr=inet_addr("0.0.0.0");
if(-1==bind(sfd,(struct sockaddr*)&addr,sizeof(addr)))
{
perror("bind failed\n");
exit(-1);
}
bind函数的参数比较复杂,有必要讲解一下:
sockfd: 在第一步的创建套接字中的返回值
struct sockaddr *addr : 这是一个结构体地址类型。比较复杂,首先 sockaddr 这个结构体原型为:
struct sockaddr {
sa_family_t sa_family; //协议族
char sa_data[14];
}
需要使用到的是sa_family这个成员,但是这个成员的赋值需要另一个结构体进行赋值,原型如下:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
其实我们真正需要的数据是来源于这个结构体,但是bind函数中需要的类型与这个结构体的类型不匹配,所以我们需要对成员赋值后再对这个结构体的类型进行强制转换成函数所需要的类型(没办法,函数需要就只能绕弯),在对第二个成员 sin_port 进行赋值端口号时,注意其类型是网络字节序,但我们赋值的端口号为主机字节序(9527),所以就需要使用 htons() 函数对其进行转换,另外在对第三个变量sin_addr 赋值时需注意其类型为结构体类型,其原型为:
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
再再再需要注意的是我们对 成员s_addr 赋值时,系统需要的是网络字节序,而我们赋值的是一个字符串"0.0.0.0",所以还需要另外一个函数进行转换 inet_addr() ,这个函数就能将我们输入的地址变为计算机所能识别的内容,还有一个需要注意,"0.0.0.0"代表系统会自动分配绑定的地址。成员变量赋值完之后,之前说过由于我们需要的是 struct sockaddr * 类型的参数,所以最后一步我们将已经赋值好的结构体变量 addr 通过 (struct sockaddr * )&addr 即可。
addlen : 即刚定义的结构体数组的大小,使用sizeof() 即可求得
3,监听套接字
监听套接字使用的函数是listen ,函数原型及使用方法如下:
int listen(int sockfd, int backlog); //函数原型
if(-1==listen(sfd ,5)) //函数的使用
{
perror("listen failed\n");
exit(-1);
}
这个函数比较简单
sockfd: 即第一步返回的套接字
backlog :客户端队列的长度(当前允许的最大客户端的个数,超过后便不会将超过的其放入队列)
4,接收客户端连接
使用 reccept 函数接受客户端的连接 ,函数原型和使用如下
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //函数原型
socklen_t len=sizeof(addr);
if(-1==accept(sfd , (struct sockaddr*)&addr ,&len))
{
perror("accept failed\n");
exit(-1);
}
sockfd :同上
*addr :同上
*addrlen : 表示结构体addr 的字节大小的地址,因此先利用一个变量对其大小进行保存
5,与客户端进行数据交互
与客户端的交互就是客户端对服务器发送消息后服务器能够接收到并将其显示。可以使用两组函数进行数据的发送和接收 read / write , send /recv ,我们使用read /write 进行交互。先定义一个数组用于存放客户端发送,使用read 对客户端发送的消息进行读取操作,存放到 buf 后再打印到终端进行验证。再对打印信息进行稍微修饰一下。
int ret;
if(0==(ret=(read(afd,buf,sizeof(buf)))))
{
printf("1连接已断开,等待连接....\n");
break;
}
if((strncmp(buf,"exit",4)==0)) //连接套接字,通信管道ID,read函数默认为阻塞 //模式如果缓冲区有数据就返回
{
close(sfd);
close(afd);
exit(-1);
}
if(!strncmp(buf,"quit",4))
{
printf("2连接已断开,等待连接.....\n");
break;
}
printf("get :%s\n",buf);
printf("ret =%d\n",ret);
memset(buf,0,sizeof(buf));
}
6,关闭套接字
最后的最简单
int close(int fd); //函数原型
fclose(sfd);
直接关闭套接字即可
最后我们用while 循环进行不断的读取即可,上完整代码:
#include <strings.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int sfd;
if(-1==(sfd=socket(AF_INET, SOCK_STREAM, 0))) //创建套接字
{
perror("socket failed\n");
exit(-1);
}
struct sockaddr_in add; //定义结构体变量
add.sin_family=AF_INET;
add.sin_port=htons(9527);
add.sin_addr.s_addr=inet_addr("0.0.0.0");
if(-1==bind(sfd,(struct sockaddr*)&add,sizeof(add))) //绑定
{
perror("bind failed\n");
exit(-1);
}
if(-1==listen(sfd, 5)) //监听套接字 backlog 客户端连接请求队列的长度,表示同一时刻接受客户端的请求数
{ //使套接字变成监听套接字
perror("listen failed\n");
exit(-1);
}
//接受连接
while(1)
{
struct sockaddr_in caddr;
socklen_t len=sizeof(caddr);
int afd=accept(sfd, (struct sockaddr*)&caddr,&len); //阻塞等待,接受客户端连接,建立通信管道
//如果请求队列中没有客户端请求就阻塞直到有请求就连接
if(afd==-1)
{ //成功返回消息管道的ID号
//如果不需要保存客户端的ip地址和端口号,则第2,3传为NULL
perror("accept failed\n");
exit(-1);
}
printf("afd=%d\n",afd);
printf("port=%u\naddr=%s\n",ntohs(caddr.sin_port),inet_ntoa(caddr.sin_addr));
char buf[32]={0};
//数据交互
while(1)
{
int ret;
// write(afd, buf, sizeof(buf));
if(0==(ret=(read(afd,buf,sizeof(buf)))))
{
printf("1连接已断开,等待连接....\n");
break;
}
if((strncmp(buf,"exit",4)==0)) //连接套接字,通信管道ID,read函数默认为阻塞 //模式如果缓冲区有数据就返回
{
close(sfd);
close(afd);
exit(-1);
}
if(!strncmp(buf,"quit",4))
{
printf("2连接已断开,等待连接.....\n");
break;
}
printf("get :%s\n",buf);
printf("ret =%d\n",ret);
memset(buf,0,sizeof(buf));
}
//当read,recv函数连接断开时,返回0
//关闭套接字
}
close(sfd);
return 0;
}
这样就已经完成服务器的编写了,其实比较难的就是使用绑定函数时对参数的处理,不过慢慢分析后就会恍然大悟。接下来我们使用一个客户端小程序测试一下能否接收消息。
测试成功,代表服务器编写成功!客户端的编写放在下一篇,感兴趣的大哥请关注。。