您的位置:首页 > 理论基础 > 计算机网络

unix网络编程之SocketAPI基本用法

2017-10-09 20:16 316 查看

网络分层模型

这一部分涉及内容比较多,分享一个链接,内容通俗易懂,写得很不错,各位可以先去看看,大概了解了解

互联网协议入门(通俗易懂的网络协议层次结构讲解)

预备知识

网络协议

了解一些基本的网络协议,比如以太网协议,TCP/IP协议,DNS协议,ARP协议等等,这些内容可以看上面给的链接

套接口

在linux中,套接口即主机+端口。说白了,客户端和服务器要想相互通讯,总需要知道对方的地址吧。怎么表示这个地址呢,就是用套接字。

常见的套接口地址有:通用套接口地址和ipv4套接口地址。

通用套接口地址:

顾名思义,该地址是通用的,所有的套接口地址都可以转为通用套接口地址

struct sockaddr

{

unit8_t sin_len;          //4字节

sa_fmily_t sa_family;   //4字节

char             sa_data[14];   //14字节

}


ipv4套接口地址:

该套接口地址是给ipv4用的,当然还有其他套接口地址,只不过ipv4在学习中是常用的。

struct sockaddr_in {

uint8_t                 sin_len;     //整个sockaddr_in结构体的长度

sa_family_t         sin_family;  //指定该地址家族,在这里必须设为AF_INET(ipv4)

in_port_t               sin_port;    //端口号

struct in_addr  sin_addr;    //IPV4的地址

char                        sin_zero[8]; //暂不使用,一般将其设置为0

};


可以发现:通用套接口地址结构和ipv4套接口地址结构,他们大小都是相等的。通用套接口地址中的char           sa_data[14],

就相当于ipv4套接口地址结构中的in_port_t      sin_port 和 struct  in_addr   sin_addr 和   char  sin_zero[8]。现在,你可以想明白为什么叫通用套接口地址了吧。


字节序

 大端字节序(Big Endian)

大端字节序又叫大端对齐,即高字节放在低地址,低字节放在高地址

 小端字节序(Little Endian)

小端字节序又叫小端对齐,即高字节放在高地址,低字节放在低地址

 主机字节序

不同的主机有不同的字节序,如x86为小端字节序,Motorola 6800为大端字节序,ARM字节序是可配置的。

 网络字节序

网络字节序规定为大端字节序

 字节序带来的问题

我们知道:有些机器是大端对齐,有些是小端对齐。那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。所以,这时候我们需要字节序转换函数。在本地设备中,先把本机的字节序转为网络字节序传到服务器,接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误。

字节序转换函数

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

说明:在上述的函数中,h代表host;n代表network s代表short;l代表long

地址转换函数

为什么要有地址转换函数

ip地址是32位的,我们为了表示方便,才写成了十进制点的模式

所以我们要将ip地质的十进制点模式转为32位的二进制模式

这样子服务器才会知道ip地址是多少

地址转换函数

int inet_aton(const char *cp, struct in_addr *inp); //第一种地址转换函数

struct in_addr{

u_int32_t s_addr;

}

in_addr_t inet_addr(const char *cp); //第二种地址转换函数

char *inet_ntoa(struct in_addr in); //第三种地址转换函数

套接字类型

 流式套接字(SOCK_STREAM) //TCP协议

提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。

 数据报式套接字(SOCK_DGRAM) //UDP协议

提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。

 原始套接字(SOCK_RAW)

字节序和地址转换函数举例

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>

//字节序转换函数
int main01()
{
unsigned int data = 0x12345678;
char *p = &data;

printf("%x, %x, %x  \n", p[0], p[1], p[2], p[3]);

if (p[0] == 0x78)
{
printf("small \n");
}
else
{
printf("big \n");
}
/***************************************
输出:
78,56,34
small
说明是小端对齐,低字节放在低地址
x86为小段模式
***************************************/

uint32_t mynetdat = htonl(data); //将主机字节序转为网络字节序
p = &mynetdat;
printf("%x, %x, %x  \n", p[0], p[1], p[2], p[3]);
if (p[0] == 0x78)
{
printf("small \n");
}
else
{
printf("big \n");
}
/***************************************
输出:
12,34,56
big
说明是大端对齐,低字节放在高地址
网络字节序规定为大端字节序
***************************************/
return 0;
}
/***************************************
为什么要有字节序转换函数:
我们知道:有些机器是大端对齐,有些是小端对齐。
那么,如果一个大端对齐的机器给小端对齐的机器传数据,必将导致错误。
所以,这时候我们需要字节序转换函数
在本地设备中,先把本机的字节序转为网络字节序传到服务器,
如何接受方再将网络字节序转为自己的字节序,这样子就保证了不会发生错误
***************************************/

//地址转换函数
int main()
{
//int inet_aton(const char *cp, struct in_addr *inp);
//in_addr_t inet_addr(const char *cp);
//char *inet_ntoa(struct in_addr in);

in_addr_t myint = inet_addr("192.168.6.222");
printf("%d\n", myint);

/*
struct in_addr{
u_int32_t   s_addr;
}
*/
struct in_addr myaddr;
inet_aton("192.168.6.222", &myaddr);
printf("%d\n", myaddr.s_addr);

printf("%s\n", inet_ntoa(myaddr));

return 0;
}
/***************************************
为什么要有地址转换函数 :
tcp协议是32位的
所以我们要将ip地质的十进制点模式转为32位的二进制模式
这样子服务器才会知道ip地址是多少
***************************************/


SocketApi基本编程模型

CS模型(客户端client/服务器service模式)



客户和服务器之间要想相互通讯,首先要搭建环境咯,如何搭建呢?

 第一步

服务器端:socket():这个函数意思是创建一个监听套接字。相当于在家安装电话,等朋友打过来。

客户端:socket():客户端也有创建一个套接字。相当于在家安装电话,以便可以给朋友打电话。

 第二步

服务器端:bind():bind函数是用与绑定端口,端口是什么上面链接有介绍。相当于我有那么多朋友,我当然要知道我在等哪个朋友电话咯。

客户端:不需要做什么

 第三步

服务器端:listen():listen函数用来监听连接请求。相当于时刻等待电话响起。

客户端:不需要做什么

 第四步

服务器端:accept():accept函数用来生成连接套接字,即主动套接字,这个套接字用来进行收发数据。如果此时客户端没有尝试连接(即调用connect函数),那么服务器端将处于阻塞状态。相当于电话响了,我要拿起电话来接听。

客户端:connect():connect函数用来发送连接请求。相当于给朋友打电话。

以上四部完成时,环境搭建完毕

 第五步

服务器端:read(), write():读数据写数据

客户端:read(), write():读数据写数据

 第六步

服务器端:关闭监听套接字和关闭主动套接字

客户端:关闭套接字

简单服务器模型



协议族和套接字类型



SocketAPI函数

服务器端:

#include <sys/socket.h>
int socket(int domain , int type, int protocol);


功能:

创建一个套接字用于通信,被动套接字(监听套接字)

返回值:

成功返回非负整数, 它与文件描述符类似,

我们把它称为套接口描述字,简称套接字。失败返回-1

参数:

domain :指定通信协议族(protocol family)(ipv4,ipv6等等)

type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW

protocol :协议类型(一般填0)

Example:

socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0


#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);


功能:

设置地址复用,使服务器关闭,客户端未关闭时,可以重启服务器

若没设置地址复用,则服务器关闭,客户端未关闭时,服务器不能重启,必须全部关闭服务器和客户端,然后重新连接

一般放在socket函数之后,bind函数之前

返回值:

成功返回0,不成功返回-1

参数:

sockfd:返回的套接字

level:一般是填SOL_SOCKET

optname:一般是填SO_REUSEADDR

Example:

int optval = 1;

if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)

{

perror("setsockopt bind\n");

exit(0);

}


#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


功能:

绑定一个本地地址到套接字

返回值:

成功返回0,失败返回-1

参数:

sockfd:socket函数返回的套接字

addr:要绑定的地址

//通用套接口地址结构

struct sockaddr

{

unit8_t sin_len;

sa_fmily_t sa_family;

char sa_data[14];

}

//IPv4套接口地址结构 具体请查看:man 7 ip

struct sockaddr_in {

uint8_t sin_len; //整个sockaddr_in结构体的长度

sa_family_t sin_family; //指定该地址家族,在这里必须设为AF_INET(ipv4)

in_port_t sin_port; //端口号

struct in_addr sin_addr; //IPV4的地址

char sin_zero[8]; //暂不使用,一般将其设置为0

};

//IP地址

struct in_addr {

u_int32_t s_addr; //IP地址,32位

};

addrlen:地址长度

Example:

struct sockaddr_in svraddr;

svraddr.sin_family = AF_INET;

svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数

svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);

//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( bind(sockfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0 )
{
perror("func bind: ");
exit(0);
}


#include <sys/socket.h>
int listen(int sockfd, int backlog);


功能:

监听

返回值:

成功返回0,不成功返回-1

参数:

sockfd:socket函数返回的套接字

backlog: 内核规定的套接字的最大连接个数

进程正在处理一个连接请求的时候,可能还存在其它的连接请求。

因为TCP连接是一个过程,所以可能存在一种半连接的状态,

有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。

如果这个情况出现了,服务器进程希望内核如何处理呢?

内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接

但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),

这样的一个队列内核不可能让其任意大,

所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。

对于给定的监听套接字,内核要维护两个队列:
1 已有客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程。
2 已完成连接的队列。


Example:

listen(sockfd, SOMAXCONN);

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);


功能:

从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞。

返回值:

成功返回非负整数,是一个描述符,这个描述即生成的主动套接字

失败返回-1

参数:

sockfd:服务器套接字,socket返回的套接口描述字

addr:将返回对等方的套接字地址

addrlen:返回对等方的套接字地址长度

Example:

struct sockaddr_in perraddr;

socklen_t perrlen;

perrlen = sizeof(perrlen);

accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);

客户端:

#include <sys/socket.h>
int socket(int domain , int type, int protocol);
功能: 创建一个套接字用于通信,被动套接字(监听套接字) 返回值: 成功返回非负整数, 它与文件描述符类似, 我们把它称为套接口描述字,简称套接字。失败返回-1 参数: domain :指定通信协议族(protocol family)(ipv4,ipv6等等) type:指定socket类型,流式套接字SOCK_STREAM(TCP协议),数据报套接字SOCK_DGRAM(UDP协议),原始套接字SOCK_RAW protocol :协议类型(一般填0) Example: socket(PF_INET, SOCK_STREAM, 0); //ipv4,TCP协议,0
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);


功能:

建立一个连接至addr所指定的套接字

返回值:

成功返回0,失败返回-1

参数:

sockfd:未连接套接字

addr:要连接的套接字地址

addrlen:第二个参数addr长度

Example:

struct sockaddr_in svraddr;

svraddr.sin_family = AF_INET;

svraddr.sin_port = htons(8001); //端口号大于1024即可,调用字节序转换函数

svraddr.sin_addr.s_addr = inet_addr(“172.0.0.1”);

connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr));

下面请看具体例子

服务器端:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main()
{
//创建监听套接字
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}

int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}

//绑定端口号
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func bind: ");
exit(0);
}

//设置监听
//一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
//listen管理了两个队列
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("func listen: ");
exit(0);
}

//等待接受,完成连接
struct sockaddr_in perraddr;
socklen_t         perrlen;
perrlen = sizeof(perrlen);
unsigned int conn;
conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
if (conn == -1)
{
perror("func accept: ");
exit(0);
}
printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号

// 收发数据
char revbuf[1024] = {0};
while (1)
{
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
printf("client already close\n");
exit(0);
}
else if (ret < 0)
{
perror("read fail:");
exit(0);
}
fputs(revbuf, stdout); //服务器端打印收到的数据
write(conn, revbuf, ret); //服务器端回发报文
memset(revbuf, 0, sizeof(revbuf));
}

close(conn);
close(sockfd);

return 0;
}


客户端

int main()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}

struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func connect: ");
exit(0);
}

char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
{
write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
fputs(revbuf, stdout);

memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}

close(sockfd);

return 0;
}


为了实现多用户使用服务器,我们可以用fork方法,每连接一次便fork一次,让一个进程去管理一个用户,示例如下:

服务器端:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>

int main()
{
//创建监听套接字
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}

int optval = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
perror("setsockopt bind\n");
exit(0);
}

//绑定端口号
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( bind(sockfd, (const struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func bind: ");
exit(0);
}

//设置监听
//一旦调用了listen函数,这个套接字sockfd将变成被动套接字,只能接受,不能发送消息
//listen管理了两个队列
if (listen(sockfd, SOMAXCONN) < 0)
{
perror("func listen: ");
exit(0);
}

//等待接受,完成连接
// 收发数据
struct sockaddr_in perraddr;
socklen_t         perrlen;
perrlen = sizeof(perrlen);
unsigned int conn;

while (1)
{
conn = accept(sockfd, (struct sockaddr *)&perraddr, &perrlen);//accept返回一个新的连接,这个连接时一个主动套接字
if (conn == -1)
{
perror("func accept: ");
exit(0);
}
printf("perradd:%s, perrport:%d\n", inet_ntoa(perraddr.sin_addr), ntohs(perraddr.sin_port)); //打印客户端ip地址,端口号

//每来一个连接,fork一次
int pid = fork();
if (pid == 0) //子进程用于收发数据
{
while(1)
{
close(sockfd); //子进程不需要监听
char revbuf[1024] = {0};
int ret = read(conn, revbuf, sizeof(revbuf));
if (ret == 0)
{
//如果在读的过程中,对方已经关闭,tcp/ip协议会返回一个数据包
printf("client already close\n");
exit(0);
}
else if (ret < 0)
{
perror("read fail:");
exit(0);
}
fputs(revbuf, stdout); //服务器端打印收到的数据
write(conn, revbuf, ret); //服务器端回发报文
memset(revbuf, 0, sizeof(revbuf));
}

}
else if (pid > 0) //父进程只用于监听,关闭主动套接字
{
close(conn);
}
else
{
printf("fork fail\n");
close(conn);
close(sockfd);
exit(0);
}
}

return 0;
}


客户端

int main()
{
int sockfd = 0;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror("func socket: ");
exit(0);
}

struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(8001);  //端口号大于1024即可,调用字节序转换函数
svraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//svraddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址

if ( connect(sockfd, (struct sockaddr *)(&svraddr), sizeof(svraddr)) < 0 )
{
perror("func connect: ");
exit(0);
}

char revbuf[1024] = {0};
char sendbuf[1024] = {0};
while( fgets(sendbuf, sizeof(sendbuf), stdin) != NULL )
{
write(sockfd, sendbuf, strlen(sendbuf)); //向服务器写数据
read(sockfd, revbuf, sizeof(revbuf)); //从服务器读数据
fputs(revbuf, stdout);

memset(revbuf, 0, sizeof(revbuf));
memset(sendbuf, 0, sizeof(sendbuf));
}

close(sockfd);

return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: