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; }
相关文章推荐
- TCP网络编程流程及Socket API基本用法
- Twig模版引擎简介:基本API用法 - Twig使用指南 3ff0
- Socket基本用法及实现长链接
- SocketAPI,CAsyncSocket,CSocket内幕及其用法
- UNIX网络编程——SOCKET API和TCP STATE的对应关系_三次握手_四次挥手及TCP延迟确认
- Linux Socket编程(不限Linux)基本函数用法
- Socket API,CAsyncSocket,CSocket内幕及其用法
- SocketAPI,CAsyncSocket,CSocket内幕及其用法
- [收藏]Socket API,CAsyncSocket,CSocket内幕及其用法
- Linux Socket编程(不限Linux)基本函数用法
- 网络通信及Socket基本API
- QTcpSocket、QTcpServer基本用法
- 网络应用的Socket API编程的基本调用流程
- Hibernate的基本API的用法
- Unix网络编程--文件IO(1) 基本API
- Linux Socket编程(不限Linux)基本函数用法
- UNIX网络编程——SOCKET API和TCP STATE的对应关系_三次握手_四次挥手及TCP延迟确认
- 网络编程socket基本API详解
- Linux Socket编程(不限Linux)基本函数用法
- UNIX网络编程——SOCKET API和TCP STATE的对应关系_三次握手_四次挥手及TCP延迟确认