目录
一、C/S模型和B/S模型
C/S | B/S | |
优点 | 缓存大量数据、协议选择灵活、 速度快 | 安全性、跨平台、开发工作量小 |
缺点 | 安全性 | 不能缓存大量数据、严格遵守http |
二、预备知识
1 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
上图为数据0xabcd分别在大端字节序和小端字节序的存储方式。小端字节序转为大端字节序,只需要按字节的形式,将小端(低地址)的数据存放在大端(高地址),实际的转换流程如下图所示。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如对于UDP段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03,地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序的问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//本地字节序--->网络字节序(IP)
uint16_t htons(uint16_t hostshort);//本地字节序---->网络字节序(port)
uint32_t ntohl(uint32_t netlong);//网络字节序--->本地字节序(port)
uint16_t ntohs(uint16_t netshort);//网络字节序--->本地字节序(IP)
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
2 IP地址转换函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
支持IPv4和IPv6
可重入函数
描述:
本地字节序(string ip)--->网络字节序
参数:
af AF_INET、AF_INET6
src 传入,IP点分十进制字符串
dst 传出,转换后的网络字节序IP
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
描述:
网络字节序--->本地字节序(string ip)
参数:
af AF_INET、AF_INET6
src 传入,网络字节序IP
dst 传出,IP点分十进制字符串
3 sockaddr数据结构
strcut sockaddr很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。数据结构定义如下所示:
man 7 ip
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET/AF_INET6 */
in_port_t sin_port; /* 端口的网络字节序 。htons(9527)*//*ip地址的网络字节序。
int dst;
inet_pton(AF_INET, "192.168.0.1", (void *)&dst);
sin_addr.s_addr = dst;
以上三步即可,不过一般使用宏定义INADDR_ANY。可以去除系统中有效的任意IP地址。
sin_addr.s_addr = htonl(INADDR_ANY);
*/
struct in_addr sin_addr; /*ip地址的网络字节序。int dst; inet_pton(AF_INET, "192.168.0.1", (void *)&dst); sin_addr.s_addr = dst*/
};/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
三、网络套接字(socket)
1 socket模型创建流程图
2 socket函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
domain 通信使用的协议版本,可以有如下选项
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用
type 协议类型,可以有如下选项
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
protocol 0 默认协议
返回值:
成功返回一个新的文件描述符,失败返回-1,设置errno
2 bind函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述:
绑定地址结构(IP+端口)
参数:
sockfd socket文件描述符
addr 构造出IP地址加端口号
addrlen sizeof(addr)长度
返回值:
成功返回0,失败返回-1, 设置errno
常见的使用方式如下所示:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8000);
3 listen函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
描述:
设置最大监听数
参数:
sockfd socket文件描述符
backlog 排队建立3次握手队列和刚刚建立3次握手队列的链接数和
4 accept函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
描述:
设置最大监听数
参数:
sockfd socket文件描述符
addr 传出。成功与服务器建立连接的那个客户端的地址结构
addrlen 传入传出。入:addr的大小。出,客户端addr实际大小
返回值:
成功返回能与服务器进行数据通信的socket对应的文件描述符,失败返回-1,并设置errno。
5 connect函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述:
使用现有的socket与服务器建立连接
参数:
sockfd socket函数的返回值
addr 传入,服务器的地址结构
addrlen addr的大小
四、代码实现
1 server端
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 128);
printf("Accepting connections ...\n");
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
while (1) {
n = read(connfd, buf, sizeof(buf));
write(STDOUT_FILENO,buf,n);
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(connfd, buf, n);
}
close(connfd);
return 0;
}
2 client端
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#define SERV_PORT 8000
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int cfd;
int conter = 10;
char buf[BUFSIZ];
struct sockaddr_in serv_addr; //服务器地址结构
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
//inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr.s_addr);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1)
sys_err("socket error");
int ret = connect(cfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret != 0)
sys_err("connect err");
while (--conter) {
write(cfd, "hello\n", 6);
ret = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, ret);
sleep(1);
}
close(cfd);
return 0;
}