socket套接字是一种网络IPC,既可以在计算机内通信,也可以在计算机间通信。socket接口可以采用许多不同的网络协议,如常见的TCP/IP协议。
类似于文件描述符,访问socket也有对应的socket描述符。要创建一个套接字,调用socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
成功返回socket描述符,失败返回-1。
参数domain,即域,指定通信的特性,包括地址格式。各个域有自己的格式表示地址,而表示各个域的常数都以AF_开头,意思是地址家族,代表英文单词Address和Family。在Linux上,通过man查得有如下域:
Name Purpose Man page
AF_UNIX, AF_LOCAL Local communication unix(7)
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_IPX IPX - Novell protocols
AF_NETLINK Kernel user interface device netlink(7)
AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
AF_AX25 Amateur radio AX.25 protocol
AF_ATMPVC Access to raw ATM PVCs
AF_APPLETALK Appletalk ddp(7)
AF_PACKET Low level packet interface packet(7)
POSIX.1还包括AF_UNSPEC,可以代表任何域。
参数type确定套接字的类型,进一步确定通信特征。通过man查得有如下类型:
SOCK_STREAM:Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.
SOCK_DGRAM :Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET :Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call.
SOCK_RAW:Provides raw network protocol access.
SOCK_RDM:Provides a reliable datagram layer that does not guarantee ordering.
SOCK_PACKET:Obsolete and should not be used in new programs; see packet(7).
SOCK_NONBLOCK:Set the O_NONBLOCK file status flag on the new open file description. Using this flag saves extra calls to fcntl(2) to achieve the same result.
SOCK_CLOEXEC:Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.
较为常见的是 SOCK_DGRAM和SOCK_STREAM,前者表示长度固定的、无连接的不可靠报文传递,后者表示有序、可靠、双向的面向连接字节流。
参数protocol通常是零,表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特性协议。在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP传输控制协议, 在AF_INET通信域中套接字类型 SOCK_DGRAM的默认协议是UDP用户数据报协议。
对于数据报接口,与对方通信时是不需要逻辑连接的,只需要送出一个报文,其地址是一个对方进程所使用的套接字,因此数据报提供了一个无连接的服务。而字节流要求在交换之前,在本地套接字和与之通信的远程套接字之间建立一个逻辑连接。
数据报是一种自包含报文,发送数据报近似于给某人邮寄邮件,可以邮寄很多信,但不能保证投递的次序,并且可能有些信件丢失在路上,每封信件包含接收者的地址,使这封信件独立于所有其它信件,每封信件可能送达不同的接收者。相比之下,使用面向连接的协议通信就像与对方打电话,首先需要通过电话建立一个连接,连接建立好之后,彼此能双向通信,每个连接是端到端的通信通道,会话中不包含地址信息,就像呼叫的两端存在一个点对点虚拟连接,并且连接本身暗含特定的源和目的地。
套接字通信是双向的,可以采用shutdown函数来禁止套接字上的输入输出,函数如下:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
参数how可以是SHUT_RD、SHUT_WR、SHUT_RDWR,分别表示关闭读、写、读写。
字节序是一个处理器架构特性,用于指示像整数这样的大数据类型的内部字节顺序。如果处理器架构支持大端字节序,那么最大字节地址对应于数字最低有效字节,小端字节序则相反,数字最低字节对应于最小字节地址。注意不管字节如何排序,数字最高位总是在左边,最低位总是在右边。
运行在同一台计算机上的进程相互通信时,一般不用考虑字节序。网络协议指定了字节序,因此异构计算机系统能够交换协议信息而不会混淆字节序。TCP/IP协议采用大端字节序,应用程序交换格式化数据时,字节序可能就会出问题,所以需要在处理器和网络之间进行字节序转换,下面是四个转换函数:
#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表示网络net,l表示长整型long,s表示短整型short。
地址标识了特定通信域中的套接字特点,地址格式与特定的通信域相关。为使不同格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构sockaddr表示:
struct sockaddr {
sa_family_t sa_family;
char sa_data[]; // variable length
...
}
sockaddr中, sa_data长度是可变的,在Linux中为14,还可以自由地添加额外的成员。
netinet/in.h头文件中定义了IPv4因特网域AF_INET的套接字地址结构sockaddr_in:
struct in_addr {
in_addr_t s_addr;
}
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
}
在Linux下, sockaddr_in定义如下:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
sin_zero为填充字段,必须全部被置为0。
有时候需打印出能被人所理解的地址格式,如函数inet_addr和inet_ntoa,用于在二进制地址格式与点分十进制字符串表示之间相互转换,但它们仅用于IPv4,功能相似的两个函数inet_ntop和inet_pton则支持IPv4和IPv6。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
int inet_pton(int af, const char *src, void *dst);
n表示网络字节序的二进制地址,p表示文本字符串格式。
理想情况下,应用程序不需要了解套接字地址的内部结构,如果应用程序只是简单地传递类似于sockaddr结构的套接字地址,并且不依赖与任何协议相关的特性,那么可以与提供相同服务的许多不多协议协作。
计算机网络配置信息可能存放在许多地方,无论这些信息放在何处,调用相关函数都能够访问它们。通过调用gethostent函数,便可以找到给定计算机的主机信息。
#include <netdb.h>
struct hostent *gethostent(void);
void sethostent(int stayopen);
void endhostent(void);
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
如果主机数据文件没有打开,gethostent会打开它,返回文件的下一个条目,返回类型为指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区,每次调用都会覆盖这个缓冲区,返回的地址采用网络字节序。函数endhostent将关闭文件。除了上面提到的几个函数,还有gethostbyname和gethostbyaddr有类似的功能,不过它们可能被认为过时而替换为如下函数:
#include <netdb.h>
struct netent *getnetent(void);
struct netent *getnetbyname(const char *name);
struct netent *getnetbyaddr(uint32_t net, int type);
void setnetent(int stayopen);
void endnetent(void);
struct netent {
char *n_name; /* official network name */
char **n_aliases; /* alias list */
int n_addrtype; /* net address type */
uint32_t n_net; /* network number */
}
协议名字和协议号采用下列函数映射:
#include <netdb.h>
struct protoent *getprotoent(void);
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
void setprotoent(int stayopen);
void endprotoent(void);
struct protoent {
char *p_name; /* official protocol name */
char **p_aliases; /* alias list */
int p_proto; /* protocol number */
}
服务是由地址的端口号部分表示的,每个服务由一个唯一的、熟知的款口号来提供。采用函数getservbyname可以将一个服务名字映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,或者采用函数getservent顺序扫描服务数据库。
#include <netdb.h>
struct servent *getservent(void);
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);
void setservent(int stayopen);
void endservent(void);
struct servent {
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port number */
char *s_proto; /* protocol to use */
}
函数getaddrinfo允许将一个主机名字和服务名字映射到一个地址:
#include <netdb.h>
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
freeaddrinfo用来释放一个或多个addrinfo结构,出错时不能使用perror函数或者strerror函数来生成错误消息,要用上面的gai_strerror函数。
函数getnameinfo则将地址转换成主机名或者服务名:
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *sa, socklen_t salen,
char *host, size_t hostlen,
char *serv, size_t servlen, int flags);
绑定地址到套接字使用函数bind:
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
调用函数getsockname可以发现绑定到一个套接字的地址,如果套接字已经和对方连接,则调用getpeername函数。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
如果处理的是面向连接的网络服务,如SOCK_STREAM,在开始交换数据以前,需要在客户端和服务器之间建立一个连接,调用connect函数。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在connect中所指定的地址是想与之通信的服务器地址,如果sockfd没有绑定到一个地址,connect会给调用者绑定到一个默认地址。connect时有可能失败,下面的例子使用了指数补偿的算法进行连接。
#include <stdio.h>
#include <sys/socket.h>
#define MAXSLEEP 128
int connect_retry(int sockfd, const struct sockaddr *addr, socklen_t alen)
{
int nsec;
for (nsec = 1; nsec <= MAXSLEEP; nsec <<= 1) {
if (0 == connect(sockfd, addr, alen)) {
return 0;
}
if (nsec <= MAXSLEEP / 2) {
sleep(nsec);
}
}
return -1;
}
connect成功后,服务器调用listen开始监听,宣告可以接受连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
参数backlog表示同时可接受的最大请求数量,这个值有上限,依据系统而定。如果请求队列已满,系统会拒绝多余连接请求。
一旦服务器调用了listen,套接字就能接收连接请求,使用accept函数获得连接请求并建立连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept失败返回-1,成功返回套接字描述符,该描述符连接到调用connect的客户端,并且和原始套接字,即accept参数sockfd具有相同的套接字类型和地址族。原始套接字没有关联到这个连接,而是继续保持可用状态并接受其它连接请求。accept是个阻塞函数,如果服务器调用accept并且当前没有连接请求,服务器会阻塞直到一个请求到来。
数据传输有下列几个函数:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
send和recv用于面向连接的套接字,sendto和recvfrom用于无连接的套接字,sendmsg和recvmsg用来指定多重缓冲区传输数据。上面函数的flags参数有以下几个选项:
MSG_OOB:如果协议支持,接收带外数据。
MSG_PEEK:返回报文内容而不真正取走报文。
MSG_TRUNC:即使报文被截断,要求返回的是报文的实际长度。
MSG_WAITALL:等待直到所有的数据可用,仅SOCK_STREAM。
MSG_CTRUNC:控制数据被截断。
MSG_DONTWAIT:recvmsg处于非阻塞模式。
MSG_EOR:接收到记录结束符。
面向连接(TCP)的服务器步骤:
(1)创建套接字socket。
(2)把地址绑定到套接字bind。
(3)监听连接listen。
(4)接收客户端的连接accept。
(5)数据传输recv、send。
(6)结束。
面向连接(TCP)的客户端步骤:
(1)创建套接字socket。
(2)连接服务器connect。
(3)数据传输recv、send。
(4)结束。
无连接(UDP)的服务器步骤:
(1)创建套接字socket。
(2)把地址绑定到套接字bind。
(3)数据传输recvfrom、sendto。
(4)结束。
无连接(UDP)的客户端步骤:
(1)创建套接字socket。
(3)数据传输recvfrom、sendto。
(3)结束。
下面是一个TCP&IP面向连接的例子,客户端向服务器发送一个字母(没有做输入合法性验证),服务器判断字母大小写,并进行大小写转换,转换结果再发送给客户端,客户端输入数字0时结束,运行客户端时要通过命令行参数指定要连接的服务器IP。
// client
// character convertion between upper and lower
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SOCKPORT 8000
int main(int argc, char** argv)
{
int sockfd;
struct sockaddr_in servaddr;
char sendletter;
char recvletter;
if (argc != 2) {
printf("usage: ./client <server_ip>\n");
return -1;
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("socket error: %s\n", strerror(errno));
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SOCKPORT);
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) {
printf("inet_pton error: %s, ip is %s\n", strerror(errno), argv[1]);
close(sockfd);
return -1;
}
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
printf("connect error: %s\n", strerror(errno));
close(sockfd);
return -1;
}
while (1) {
printf("\ninput a character:\n\talphabet - convert between upper and lower\n\tdigit 0 - quit\n");
printf("\n\tyour characer: ");
scanf("%c", &sendletter);
getchar();
if (strncmp(&sendletter, "0", 1) == 0) {
printf("quit\n");
break;
}
if (send(sockfd, &sendletter, sizeof(sendletter), 0) == -1) {
printf("send error: %s\n", strerror(errno));
break;
}
if (recv(sockfd, &recvletter, sizeof(recvletter), 0) == -1) {
printf("recv error: %s\n", strerror(errno));
break;
}
printf("\tfrom [%c] to [%c]\n", sendletter, recvletter);
}
close(sockfd);
return 0;
}
// server
// character convertion between upper and lower
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define SOCKPORT 8000
int main(int argc, char** argv)
{
int sockfd;
int clifd;
char srcletter;
char desletter;
struct sockaddr_in servaddr;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("socket error: %s\n", strerror(errno));
return -1;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SOCKPORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY native ip
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
printf("bind error: %s\n",strerror(errno));
close(sockfd);
return -1;
}
if (listen(sockfd, 1) == -1) {
printf("listen error: %s\n", strerror(errno));
close(sockfd);
return -1;
}
printf("===== waiting for client‘s request =====\n");
if((clifd = accept(sockfd, (struct sockaddr*)NULL, NULL)) == -1) {
printf("accept error: %s\n", strerror(errno));
close(sockfd);
return -1;
}
while (1) {
if (recv(clifd, &srcletter, sizeof(srcletter), 0) == -1) {
printf("recv error: %s\n", strerror(errno));
break;
}
if (isupper(srcletter)) {
desletter = tolower(srcletter);
}
else if (islower(srcletter)) {
desletter = toupper(srcletter);
}
printf("recv [%c] send[%c]\n", srcletter, desletter);
if (send(clifd, &desletter, sizeof(desletter), 0) == -1) {
printf("send error: %s\n", strerror(errno));
break;
}
}
close(clifd);
close(sockfd);
return 0;
}
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文地址:http://blog.csdn.net/ieearth/article/details/46829649