从零开始的Socket编程 一
使用Socket
如前文所述,Socket用于在网络上连接两个不同的进程,将这个两个经常分别称为Server和Client,Server就像是插座后面的电源,等待者插头,也就是Client来进行连接。只有当Server和Client成功建立起连接,这时两个进程才可以进行通信。
由于两个进程扮演了不同的角色,需要提供不同的功能,因此Server和Client创建Socket的方式也不同。
从图中可以图中可以看出要使用socket进行通信的基本流程。
一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定
要构成一个连接需要两个socket,其中一个先创建并一直等待,称为server; 另一个在需要建立连接的时候主动发起连接,称为client
server端通过socket()
函数创建一个socket,然后调用bind()
告知socket应该在哪个地址上进行监听,之后再调用listen()
启动监听, 在默认没开启非阻塞模式的情况下,accept()
会等到接受到一个来自client的连接请求后返回一个新socket.至此,server的准备工作完成,在调用accept()
后,server会阻塞直到由连接进来.
server端建立好socket后,需要client主动发起连接后才能构成一个可通信的连接.client端同样也需要调用socket()
创建一个socket, 但是不需要调用bind()
, listen()
, accept()
来设置监听地址和等待连接.
在Linux/Unix下,Socket被看作时文件,因此接下里的通信操作类似与文件IO,通过read()
和write()
来完成,只不过socket会提供其他的函数来提供更多的功能. 在通信结束后,类似与文件读写完成后,需要调用close()
来关闭socket.
Socket 的基本操作
在上文的图中也包括了socket的基本操作,以及这些操作在socket通信过程中所处的位置。
socket
int socket (int __domain, int __type, int __protocol)
该函数创建一个socket描述符(socket descriptor),该描述符唯一标识一个socket,在Linux下该接口的声明为:
/* Create a new socket of type TYPE in domain DOMAIN, using
protocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.
Returns a file descriptor for the new socket, or -1 for errors. */
extern int socket (int __domain, int __type, int __protocol) __THROW;
参数
- __domain : 协议域,又称协议族,定义在"sys/socket.h"(Windows系统在winsock2.h)文件中,常用的协议族有:
AF_INET
(IPv4),AF_INET6
(IPv6),AF_LOCAL
(用于同一台主机上的进程间通信,详细可参考https://blog.csdn.net/frank_jb/article/details/77199834)等。协议族指定了socket的地址类型,在通信中必须采用对应的地址。 - __type : 指定socket的类型。常用的有
SOCK_STREAM
——面向连接的,常用于TCP,SOCK_DGRAM
——面向报文的,常用于UDP等。这些类型定义在socket_type.h
文件中 - __protocol : 指定协议。常用协议有
IPPROTO_TCP
——TCP协议,IPPROTO_UDP
——UDP协议,IPPROTO_TIPC
——TIPC(Transparent Inter Process Communication)协议
上述type和protocol不可以自由组合,当protocol为0时会根据type选择默认的协议。
返回值
返回新创建的socket描述符,若创建失败则返回-1
bind
对于server端进程而言,在创建socket后需要将协议族中一个特定的地址赋予socket,在改地址上等待client的连接
/* Give the socket FD the local address ADDR (which is LEN bytes long). */
extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)
- __fd : 文件描述符,即socket描述符,调用
socket()
的返回值 - __addr :
#define __CONST_SOCKADDR_ARG const struct sockaddr *
,指定要绑定的地址,该结构根据创建socket时的地址协议族的不同而不同。如IPv4对应的是
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
而IPv6对应的是:
#if !__USE_KERNEL_IPV6_DEFS
/* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
#if !__USE_KERNEL_IPV6_DEFS
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};
#endif /* !__USE_KERNEL_IPV6_DEFS */
- __len :对应的地址的长度
listen
/* Prepare to accept connections on socket FD.
N connection requests will be queued before further requests are refused.
Returns 0 on success, -1 for errors. */
extern int listen (int __fd, int __n) __THROW;
server调用listen
来监听socket。socket
创建的socket默认为主动发起请求的,listen
使得socket变为被动接收请求,等待连接的到来。
- __fd : socket描述符
- __n : socket可以排队的最多的连接数,超过该值后的连接将被拒绝。
成功时返回0,失败返回-1
accept
/* Await a connection on socket FD.
When a connection arrives, open a new socket to communicate with it,
set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
peer and *ADDR_LEN to the address's actual length, and return the
new socket's descriptor, or -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int accept (int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
等待连接到达,当连接到达后,建立一个新的socket用于通信。
- __fd : 服务端的socket描述符
- __addr : 指向
struct sockaddr
的指针,输出参数,用于返回发起连接的客户端地址 - __addr_len : 地址长度
成功时返回新的socket描述符,失败时返回-1
需要区分socket
返回的描述符和accept
返回的描述符。
由socket
返回的socket称为监听socket描述符,一个服务器通常只有一个监听socket描述符,在该服务器的生命周期内一直存在;
accept
返回的描述符是由操作系统为每个服务器进程接收的客户连接创建的socket描述符,当结束服务端与客户端的一次会话后对应socket描述符将被关闭。
connect
connect用于客户端向服务端主动发起连接
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).
For connectionless socket types, just set the default address to send to
and the only address from which to accept transmissions.
Return 0 on success, -1 for errors.
This function is a cancellation point and therefore not marked with
__THROW. */
extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
- __fd : 客户端通过
socket()
创建的socket描述符 - __addr : 要连接的地址的服务端的socket地址
- __len : __addr的长度
read/write
使用上述函数即可建立起网络连接,连接建立后即可进行通信,提供了多组函数进行通信:
//unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
//sys/socket.h
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, 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 recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
read
和write
是Linux上通用的文件IO函数,之前也说了,在Linux将Socket也看作是文件。
recv
, recvfrom
, recvmsg
都是用来接收数据的,在Linux上可以通过man recvfrom
查看三者的具体差别
send
, sendto
, sendmsg
又来发送数据,可通过man sendmsg
查看具体信息
总的来说sendmsg/recvmsg
能提供最多的信息
总结
本文先介绍了socket通信的基本流程,然后介绍了socket通信相关的接口.熟悉编程的同学可能已经可以据此实现简单的socket通信了,由于不希望一文的篇幅过长因此将实现通信的代码放在下一篇文章中