SOCKET编程
一、预备知识
1. 网络编程基础
- 为了简化开发通信程序的工作,由Berkely学校开发了一套网络通信程序的API函数
- 标准socket标准被扩展成window socket和unix socket
- linux中的网络编程通过socket接口实现。Socket既是一种特殊的IO,它也是一种文件描述符。
- 一个完整的Socket 都有一个相关描述{协议,本地地址,本地端口,远程地址,远程端口};每一个Socket 有一个本地的唯一Socket 号,由操作系统分配。
2. 网络字节序
- 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
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);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
//h表示host,n表示network,l表示32位长整数,s表示16位短整数。
//如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
3. IP地址转换函数
#include <arpa/inet.h>
//IP地址字符串转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
//网络字节序转换为IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
//
//两个函数都支持IPv4和IPv6
例:
struct sockaddr_in serveraddr;
//将“127.0.0.1”这个十进制点分的IP地址字符串,转换为网络字节序,存入serveraddr.sin_addr中
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
4. sockaddr_in地址结构
struct sockaddr_in {
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 (IP)*/
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
- socket地址结构体有很多种,以上只是其中的一种,各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数
AF_INET、AF_INET6、AF_UNIX
。 - 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
- 因此,socket API可以接受各种类型的sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些函数的参数都用 (struct sockaddr *)*类型表示,在传递参数之前要强制类型转换一下
- 例如:
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
二、网络套接字函数
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来传输 - 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
socket()
打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。
2. bind()
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:
- socket文件描述符
addr
:
- 构造出IP地址加端口号
addrlen
:
- sizeof(addr)长度
返回值
:
- 成功返回0,失败返回-1, 设置errno
bind()
的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
例如:
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));//将结构体清零
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP网络字节序
servaddr.sin_port = htons(8000);//端口号
3. listen()
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd
:
- socket文件描述符
backlog
:
- 排队建立3次握手队列和刚刚建立3次握手队列的链接数和
listen()
声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
listen()
成功返回0,失败返回-1。
4. accept()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockdf
:
- socket文件描述符
addr
:
- 传出参数,返回链接客户端地址信息,含IP地址和端口号
addrlen
:
- 传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小
返回值
:
- 成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno
5. connect()
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockdf
:
- socket文件描述符
addr
:
- 传入参数,指定服务器端地址信息,含IP地址和端口号
addrlen
:
- 传入参数,传入sizeof(addr)大小
返回值
:
- 成功返回0,失败返回-1,设置errno
客户端需要调用connect()
连接服务器,connect()
和bind()
的参数形式一致,区别在于
bind()
的参数是自己的地址,而connect()
的参数是对方的地址。connect()
成功返回0,出错返回-1。