前言
在TCP/IP协议中,网络层的“ip地址”可以唯一标识网络中的主机,传输层的“协议和端口”可以唯一标识主机中的进程。这样利用三元组(ip地址,协议,端口)就可以唯一标识网络的进程,网络中的进程通信就可以利用这个标志与其它程序进行交互。在这之中大部分应用都是通过socket实现的。
socket
通常称作“套接字”,用于描述ip地址和端口,是一个通信链的句柄。是使用Uinx文件描述符和其它程序通迅的方式。本质上就是一组接口,使编程简单化。
Internet套接字
流套接字(SOCK_STREAM):可靠的双向通迅的数据流。无错误传递,有自己的错误控制,使用TCP协议,只能读取此协议数据。
数据报套接字(SOCK_DGRAM):提供一种无连接的服务。不保证数据传输的可靠性,需自行处理,使用UDP协议,只能读取此协议数据。
原始套接字(SOCK_RAW):可以读写内核没有处理的ip数据包,如果需要访问其他协议(非TCP/UDP)发送数据必须使用原始套接字。
socket描述符
本质上就是一个整数,类型:int。当进程要创建套接字时,系统就提供一个小的整数作为描述符来标识这个套接字。然后进程利用该描述符来完成某种操作(如文件的读写)。
socket数据结构
套接字的内部数据结构包含很多字段,但系统创建后,大多数字段没有填写,需要进行具体配置才能使用。
struct sockaddr { unsigned short sa_family; /* 地址家族, AF_xxx */ char sa_data[14]; /*14 字节协议地址*/ }; struct sockaddr_in ("in" 代表 "Internet"。) struct sockaddr_in { short int sin_family; /* 通信类型 */ unsigned short int sin_port; /* 端口 */ struct in_addr sin_addr; /* Internet 地址 */ unsigned char sin_zero[8]; /* 为了保证结构体struct sockaddr_in的大小和结构体struct sockaddr的大小相等*/ }; struct in_addr { unsigned long s_addr; };
sa_family可以是各种类型,常用的是“AF_INET”。sa_data包含套接字中的目标地址和端口信息。
struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。
注意 :sin_zero (它被加入到这个结构,并且长度和 struct sockaddr一样) 应该使用函数 bzero() 或 memset() 来全部置零。 同时,一个指向 sockaddr_in 结构体的指针也可以被指向结构体 sockaddr 并且代替它。这样的话即使 socket() 想要的是 struct sockaddr *,你仍然可以使用 structsockaddr_in,并且在最后转换。
注意 sin_family 和 struct sockaddr 中的 sa_family 一致并能够设置为 "AF_INET"。最后,sin_port 和 sin_addr 必须是网络字节顺序 sin_family必须是本机字节顺序。因为sin_addr 和 sin_port 分别封装在包的 IP 和 UDP 层。因此,它们必须要 是网络字节顺序。但是sin_family
域只是被内核(kernel) 使用来决定在数 据结构中包含什么类型的地址,所以它必须是本机字节顺序。同时,sin_family 没有发送到网络上,它们可以是本机字节顺序。
通常的用法是:
int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); my_addr.sin_family = AF_INET; /* 主机字节序 */ my_addr.sin_port = htons(MYPORT); /* short, 网络字节序 */ my_addr.sin_addr.s_addr = inet_addr("192.168.0.1"); bzero(&(my_addr.sin_zero), 8); /* zero the rest of the struct */ //memset(&my_addr.sin_zero, 0, 8); bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
网络与本机字节顺序的转换
主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。
htons()--"Host to Network Short" //将short类型的值从主机字节序转换为网络字节序(short两个字节,即16位值)
htonl()--"Host to Network Long" (long四个字节,即32位值)
ntohs()--"Network to Host Short"
ntohl()--"Network to Host Long"
ip地址处理
inet_addr():将 IP 地址从 点数格式转换成无符号长整型。
使用方法:
sockaddr.sin_addr.s_addr = inet_addr("192.168.1.10"); //将IP地址字符串转换为long类型的网络字节序,发生错误时返回-1。
inet_ntoa(); //将long类型的网络字节序转换成IP地址字符串。
注意,inet_addr()返回的地址已经是网络字节格式,所以你无需再调用函数 htonl()。
使用方法:
printf("%s",inet_ntoa(sockaddr.sin_addr.s_addr));
注意, inet_ntoa()返回的是一个指向一个字符的 指针。它是一个由 inet_ntoa()控制的静态的固定的指针,所以每次调用 inet_ntoa(),它就将覆盖上次调用时所得的 IP 地址。
gethostname():获取程序所运行的机器的主机名字。
#include <unistd.h>
int gethostname(char *hostname, size_t size);
hostname 是一个字符数组指针,它将在函数返回时保存主机名。size 是 hostname 数组的字节长度。
函数调用成功时返回 0,失败时返回 -1,并设置 errno。
gethostbyname():获取机器的ip地址
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
其数据结构如下:
struct hostent {
char *h_name; - 地址的正式名称。
char **h_aliases; - 空字节-地址的预备名称的指针。
int h_addrtype; -地址类型; 通常是 AF_INET。
int h_length; - 地址的比特长度。
char **h_addr_list; - 零字节-主机网络地址指针。网络字节顺序。
};
#define h_addr h_addr_list[0] // - h_addr_list 中的第一地址。
gethostbyname() 成功时返回一个指向结构体 hostent 的指针,或者 是个空 (NULL) 指针。(但是和以前不同,不设置errno,h_errno 设置错误信息而是使用herror()。)
以下为使用例子:
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> int main(int argc, char *argv[]) { struct hostent *h; if (argc != 2) { /* 检查命令行 */ fprintf(stderr,"usage: getip address\n"); exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { /* 取得地址信息 */ herror("gethostbyname"); exit(1); } printf("Host name : %s\n", h->h_name); printf("IP Address : %s\n",inet_ntoa(*((struct in_addr *)h->h_addr))); //h->h_addr 是一个 char *, 但是 inet_ntoa() 需要的是 struct in_addr。故转换h->h_addr 成 struct in_addr *,然后得到数据。 return 0; }
socket基本函数
socket():
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol); //返回描述符,错误时返回-1
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
protocol:指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议。
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
bind():
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, int addrlen); //在错误的时候返回-1,并且设置全局错误变量 errno。
sockfd 是调用 socket 返回的文件描述符。my_addr 是指向数据结构 struct sockaddr 的指针,它保存你的地址(即端口和IP 地址) 信息。 addrlen 设置为 sizeof(struct sockaddr)。这里有些工作系统会自己处理:
my_addr.sin_port = 0; /* 系统随机选择一个没有使用的端口*/
my_addr.sin_addr.s_addr = INADDR_ANY; /* 使用自己的 IP 地址 ,INADDR_ANY实际上为0,不需要转网络字节顺序*/
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。另外,不要采用小于 1024 的端口号。所有小于 1024 的端口号都被系统保留!你可以选择从
1024 到 65535 的端口(如果它们没有被别的程序使用的话)。
listen()、connect():
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog); //在发生错误的时候返回-1,并设置全局错误变量 errno。
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //在错误的时候返回-1,并 设置全局错误变量 errno。
connect函数的第一个参数是系统调用 socket()返回的套接字文件描述符,第二参数为服务器的socket地址,保存着目的地端口和 IP 地址的数据结构 struct sockaddr,第三个参数为socket地址的长度,通常设置为sizeof(struct sockaddr)。客户端通过调用connect函数来建立与TCP服务器的连接。
accept():
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen); //在错误时返回-1,并设置全局错误变量 errno。
sockfd是套接字描述符。addr 是指向局部的数据结构 sockaddr_in 的指针。addrlen 是接受addr的结构的大小的,设置为 sizeof(struct sockaddr_in),也可以被设置为NULL。
注意:
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的.
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号。
数据发送
send() 和 recv()函数:用于流式套接字或者数据报套接字的通讯。
int send(int sockfd, const void *msg, int len, int flags); //返回实际发送的字节数(可能小于想发送的字节数)在错误的时候返回-1,并设置 errno。
sockfd 是你想发送数据的套接字描述符(或者是调用 socket() 或者是 accept() 返回的。)msg 是指向你想发送的数据的指针。len 是数据的长度。 把 flags 设置为 0 就可以了。
int recv(int sockfd, void *buf, int len, unsigned int flags); //返回实际读入缓冲的数据的字节数。或者在错误的时候返回-1, 同时设置 errno。
sockfd 是要读的套接字描述符。buf 是要读的信息的缓冲。len 是缓 冲的最大长度。flags 可以设置为 0。
sendto() 和 recvfrom()函数:用于无连接数据报套接字;
int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr *to, int tolen);
to 是个指向数据结构 struct sockaddr 的指针,它包含了目的地的 IP 地址和端口信息。tolen 可以简单地设置为sizeof(struct sockaddr)。其余的与send()一样。
int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);
from 是一个指向局部数据结构 struct sockaddr 的指针,
它的内容是源机器的 IP 地址和端口信息。fromlen 是个 int 型的局部指针,它的初始值为 sizeof(struct sockaddr)。函数调用返回后,fromlen 保存着实际储存在 from 中的地址的长度。其余的与recv()一样。
close()和 shutdown():关闭套接字描述符
#include <unistd.h>
int close(int fd);
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
int shutdown(int sockfd, int how);//成功时返回 0,失败时返回 -1,同时设置 errno。
sockfd 是你想要关闭的套接字文件描述复。how 的值是下面的其中之 一:
0 - 不允许接受
1 - 不允许发送
2 - 不允许发送和接受(和 close() 一样)
阻塞
阻塞就是在执行设备操作时,若不能获得资源,则进程挂起直到满足可操作条件再进行操作。很多函数都利用阻塞。像accept(),recv() 函数。当第一次调用 socket() 建 立套接字描述符的时候,内核就将它设置为阻塞。如果不想套接字阻塞,
可以调用函数 fcntl()、select(),具体的以后再研究。
Socket编程实例:
服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。当个客户端初始化一个Socket,然后连接服务器(connect)时,如果连接成功,刚客户端与服务器端的连接就建立成功了。接下来就可以进行数据的发送与接收。其流程如下:
socket简单测试程序
服务器代码:
#include <netinet/in.h> // for sockaddr_in #include <sys/types.h> // for socket #include <sys/socket.h> // for socket #include <stdio.h> // for printf 、perror #include <stdlib.h> // for exit 、perror #include <string.h> // for bzero #define SERVER_PORT 6666 #define BUFFER_SIZE 1024 #define FILE_NAME_SIZE 512 int main(int argc, char **argv) { int server_socket; struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); //设置通信类型为ipv4,端口号,ip地址 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htons(INADDR_ANY); server_addr.sin_port = htons(SERVER_PORT); //创建用于internet的流协议(TCP)的socket if (-1 == (server_socket = socket(AF_INET, SOCK_STREAM, 0))) { perror("Create Socket Failed:"); return -1; } //将套接字与服务器地址绑定 if( -1 == (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr) ))) { perror("Server Bind Port Failed!"); return -1; } //监听连接请求,监听队列长度为5 if ( -1 == (listen(server_socket, 5)) ) { perror("Server Listen Failed!"); return -1; } while (1) //服务器端要一直运行 { //定义客户端的socket描述符 int client_socket; struct sockaddr_in client_addr; int sock_length; sock_length = sizeof(client_addr); /* * 接受一个到server_socket代表的socket的一个连接 *accept函数返回一个新的socket,这个socket(client_socket)用于同连接到的客户的通信 *accept函数把连接到的客户端信息填写到客户端的socket地址结构client_addr中 */ if (-1 == (client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&sock_length))) { perror("Server Accept Failed!\n"); return -1; } printf("Conecting........OK!\n"); char buffer[BUFFER_SIZE]; bzero(buffer, BUFFER_SIZE); int length; //接收与发送数据 while ( length = (recv(client_socket, buffer, BUFFER_SIZE, 0))) { if (-1 == length) { perror("Server Recieve Data Failed!\n"); break; } buffer[BUFFER_SIZE] = '\0'; printf("%s\n", buffer); // bzero(buffer, BUFFER_SIZE); if ( -1 == send(client_socket, buffer, BUFFER_SIZE, 0) ) { perror("Write Failed!\n"); return -1; } } //关闭与客户端的连接 close(client_socket); } //关闭监听用的socket close(server_socket); return 0; }
客户端代码:
#include <netinet/in.h> // for sockaddr_in #include <sys/types.h> // for socket #include <sys/socket.h> // for socket #include <stdio.h> // for printf #include <stdlib.h> // for exit #include <string.h> // for bzero #define SERVER_PORT 6666 #define BUFFER_SIZE 1024 int main(int argc, char **argv) { if (argc != 2) { printf("Usage: ./%s ServerIPAddress\n",argv[0]); exit(1); } int client_socket; struct sockaddr_in client_addr; bzero(&client_addr,sizeof(client_addr)); //设置通信类型为ipv4,端口号(0表示系统自动分配),ip地址(INADDR_ANY自动获取) client_addr.sin_family = AF_INET; client_addr.sin_port = htons(SERVER_PORT); if(inet_aton(argv[1],&client_addr.sin_addr) == 0) //服务器的IP地址来自程序的参数 { printf("Server IP Address Error!\n"); return -1; } //创建用于internet的流协议(TCP)socket if ( -1 == (client_socket = socket(AF_INET, SOCK_STREAM, 0))) { perror("Create Socket Failed:"); return -1; } //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接 if(-1 == connect(client_socket, (struct sockaddr*)&client_addr, sizeof(struct sockaddr))) { printf("Can Not Connect To %s!\n", argv[1]); return -1; } printf("Conect to %s ..........OK!\n", argv[1]); char buffer[BUFFER_SIZE]; int length; bzero(buffer,BUFFER_SIZE); //向服务器发送数据并打印 while (1) { printf("Enter data to send: "); scanf("%s", buffer); if (!strcmp(buffer, "quit")) { break; } if ( -1 == (length = send(client_socket, buffer, strlen(buffer), 0))) { printf("Send data failed!\n"); } else { buffer[length] = '\0'; printf("Send Data: %s \n", buffer); } bzero(buffer, BUFFER_SIZE); } close(client_socket); return 0; }
只是一个小小的测试程序,关于数据的传送也只是简单的从键盘输入。