文章目录
一、socket套接字
如果学习或者了解过计算机网络就会知道分层的概念,一台计算机的数据通过层层包装之后,发送到网络上,另一台计算机将这些数据层层剥离得到真实的数据。这里简单提一下。可以参考sokcet编程http://c.biancheng.net/view/2126.html。
参考模型
上面提到的封装思想,是通过一些参考模型来实现的。两个常见的模型:OSI参考模型、TCP/IP模型。
OSI是最初为了实现计算机网络通信提出的概念模型包含7层,非常复杂难以实现,真正实现网络通信之后使用的是TCP/IP模型。在TCP/IP模型中,分为四层:网络接口层、网络层、传输层、应用层。我们使用的socket套接字在传输层,因此包含:TCP套接字、UDP套接字。
二、socket通信
1、通信流程
C/S模型下使用socket套接字进行网络通信的流程如图:
2、函数说明
上面的流程列出了具体使用到的函数,下面说明一下这些函数:
2.1socket接口
socket是插座、接口的意思,在进行通信之前,我们需要通过socket来创建一个接口,这样才可以进行后续的通信。
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数说明:
-
domain:旧版本中这个参数名为af,af也就是地址族(address family),说明这是一个IP地址。IP地址包含IPv4和 IPv6,使用前者为AF_INET,后者则为AF_INET6。
-
type:这个参数是数据传输方式/套接字类型,常用的有流式套接字(SOCK_STREAM)和数据报套接字 (SOCK_DGRAM),流式套接字使用面向连接的TCP传输协议,数据报套接字使用无连接的UDP传输协议。
-
protocol:协议,设置套接字使用的协议,常用的就是前面说的TCP和UDP协议,对应名分别为IPPROTO_TCP 、IPPROTO_UDP。
-
返回值:socket文件描述符。
有的时候确定了前面两个参数也可以不使用后面的protocol(协议),设置为0即可。系统会推出使用什么协议进行通信,如:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
2.2 bind绑定
socket通信时,server服务器端绑定客户端,然后对其进行监听。
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
-
sockfd:socket文件描述符。
-
sockaddr:
sockaddr
结构体指针,sockaddr结构体原型如下:struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
成员:sa_family说明地址类型,IPv4或IPv6,参考上一个函数。
sa_data[14]用来保存IP地址和端口号。
注意:一般使用
sockaddr_in
结构体填充地址和端口,最后强制转换为sockaddr
类型填入bind函数。 -
addrlen:前一个参数的大小一般使用sizeof(addr)。
-
返回值:成功返回0,失败返回-1,设置errno相应错误。
这里简单说明一下为什么使用sockaddr_in而不是使用sockaddr结构体填充ip地址和端口号,sockaddr_in结构体类型如下:
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
uint16_t sin_port; //16位的端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用,一般用0填充
};
比较一下就可以看出它们的区别,sockadd_in结构体包含成员sin_port和sin_addr,表示端口和ip地址,使用时直接向里面填充即可。sin_zero[8]一般不使用。
端口sin_port是16位的数据类型,端口范围为0 ~ 65535,熟知端口范围是0~1023。因为最终需要将sockaddr_in结构体转换为sockaddr结构体成员的char类型,所以端口号需要进行转换,例如: htons(1234);
ip地址使用的是点分十进制字符串,也需要通过函数进行转换,例如:inet_addr("127.0.0.1")
。
简单bind()绑定示例:
//创建套接字
int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//创建sockaddr_in结构体变量
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口
//将套接字和IP、端口绑定
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
2.3 connect连接
当client客户端对服务器发起连接请求时,需要使用connect()连接服务器。
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数类型与上一个函数相同,参考上一个函数。但需要注意的是,connect()需要设置的是服务器的地址和端口。
2.4 listen监听
当服务器和客户端建立连接后,服务器需要监听客户端的读写请求,需要使用listen()函数对客户端进行监听。
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数说明:
-
sockfd:socket文件描述符。
-
backlog:定义的请求队列的最大长度。
-
返回值:成功返回0,失败返回-1,设置errno相应错误。
2.5 accept接收
当监听到请求时,可以通过accpet函数接收请求。
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数类型说明参考connect函数和bind函数,注意最后一个参数为指针类型。
注意:accpet()接收请求成功之后,返回的是一个新的套接字,使用新的套接字和客户端进行通信,参数addr中会保存当前通信的客户端的地址和端口信息。
2.6 write写入
linux下一切皆文件,所以当我们使用socket通信需要发送数据时,使用write函数。这里的write函数与以前的文件操作的write没有区别,read也是同理。
函数原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
- fd:使用socket使用这个参数即为sockfd。
- buf:需要写入的数据,为空指针类型。
- conut:写入数据的大小。
- 返回值:写入的字节数。
2.7 read读取
接收数据使用read函数。
函数原型:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明:
- fd:使用socket使用这个参数即为sockfd。
- buf:读出的数据后存放的位置。
- conut:读取数据的大小。
- 返回值:读出的字节数。
2.8 转换函数
函数原型:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
函数描述:
htonl函数的作用是:将无符号整型hostlong主机从主机字节顺序转换为网络字节顺序。
htons函数的作用是:将无符号短整型hostshort主机从主机字节顺序转换为网络字节顺序。
ntohl函数的作用是:将无符号整数netlong从网络字节顺序转换为主机字节顺序。
ntohs函数的作用是:将无符号短整数netshort从网络字节顺序转换为主机字节顺序。
当使用端口和ip地址时需要使用这些函数转换为特定的格式。
三、通信实例
服务器端
server.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
int fdServ, fdClnt;
char bufServ[] = "I'm a server!";
char sBuf[40] = {0};
struct sockaddr_in servAddr, clntAddr;
memset(&servAddr, '\0', sizeof(servAddr));
servAddr.sin_family = AF_INET;//ipv4
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址为本机ip
servAddr.sin_port = htons(1234);//端口1234
//创建流式套接字
fdServ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//ipv4 流式套接字 TCP协议
if(bind(fdServ, (struct sockaddr *)&servAddr, sizeof(struct sockaddr)) == 0)
{ //绑定客户端成功
printf("bind success!\n");
//监听客户端请求
if(listen(fdServ, 20) == 0)
printf("Server listening...\n");
//阻塞等待请求
socklen_t clntAddr_size = sizeof(clntAddr);
fdClnt = accept(fdServ, (struct sockaddr *)&clntAddr, &clntAddr_size);
if(write(fdClnt, bufServ, sizeof(bufServ)) > 0)
{ //写入数据成功
printf("Server write success!\n");
}
else{
printf("Write failed!\n");
perror("Server write");
exit(1);
}
if(read(fdClnt, sBuf, sizeof(sBuf)) > 0)
{ //读取成功
printf("Server read data success!\n");
printf("-----------------Data from client: %s\n", sBuf);
}
}
close(fdClnt);
close(fdServ);
return 0;
}
客户端
client.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main(int argc, char *argv[])
{
int fdServ;
char bufRead[40]={0};
char cBuf[] = "I'm a client!";
struct sockaddr_in servAddr;
memset(&servAddr, '\0', sizeof(servAddr));
//创建socket套接字
fdServ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servAddr.sin_port = htons(1234);
if(connect(fdServ, (struct sockaddr *)&servAddr, sizeof(servAddr)) == 0)
{
printf("Connect to server success!\n");
if(read(fdServ, bufRead, sizeof(bufRead)) > 0)
{ //读取到数据
printf("-----------------Data from server: %s\n", bufRead);
}
// sleep(10);
printf("cBuf = %s\n", cBuf);
if(write(fdServ, cBuf, sizeof(cBuf)) > 0)
{ //写入数据成功
printf("client write data success!\n");
}
}
close(fdServ);
return 0;
}