linux下的socket通信
在linux下,无论多么复杂的服务器或客户端程序,无论什么编程语言实现的,其底层都离不开linux内核提供的系统调用(也就十多个函数),其网络通信的基本流程一定如下所述:
对于服务器,其通信流程一般有如下步骤:
- 调用socket函数创建socket,这一步会创建一个文件描述符FD。
- 调用bind函数将socket(也就是FD)绑定到某个ip和端口的二元组上。
- 调用listen函数开启侦听端口。
- 调用accept阻塞等待接受连接,当有客户端请求连接上来后,产生一个新的socket(客户端socket)。
- 基于新产生的socket调用send或recv函数开始与客户端进行数据通信。
- 通信结束后,调用close函数关闭客户端socket。
对于客户端,其通信流程一般有如下步骤:
- 调用socket函数创建客户端socket。
- 调用connect函数尝试连接服务器。
- 连接成功以后调用send或recv函数开始与服务器进行数据通信。
- 通信结束后,调用close函数关闭socket。
上述流程可以绘制成如下图示:
函数说明
socket
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数说明:
- domain:指定通信协议族。常用的协议族有AF_INET、AF_UNIX等,对于TCP协议,该字段应为AF_INET(ipv4)或AF_INET6(ipv6)。
- type:指定socket类型。常用的socket类型有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)等。
- protocol:指定socket所使用的协议,一般我们平常都指定为0,使用type中的默认协议。严格意义上,IPPROTO_TCP(值为6)代表TCP协议。
- 返回值:成功为文件描述符FD,失败未-1。
另外type参数可以附加设置socket的类型,阻塞还是非阻塞,默认是阻塞,设置为非阻塞SOCK_NONBLOCK的方法如下:
socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
bind
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数说明:
- sockfd:一般为调用socket函数返回的文件描述符。
- *addr:地址的格式与协议有关,ipv4查看
man 7 ip
,ipv6查看man 7 ipv6
。 - addrlen:*addr的长度。
- 返回值:0为成功,-1为失败。
在ip中*addr
结构体声明如下:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
在ipv6中*addr
结构体声明如下:
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
listen
函数原型:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数说明:
- sockfd:一般为调用socket函数返回的文件描述符。
- backlog:待连接的等待队列的最大值。
- 返回值:0为成功,-1为失败。
accept
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
函数说明:
- sockfd:一般为调用socket函数返回的文件描述符。
- *addr:存储客户端的信息(如ip和端口),传入一个空的结构体,便于函数内部为这个结构体赋值,这样外部就能访问了。
- *addrlen:*addr的长度。
- flags:c语言不能像java中一样方法重载,所以只能用个新的方法accept4加个flags参数,这个参数也是用来设置非阻塞SOCK_NONBLOCK的。
- 返回值:成功则返回与客户端建立连接的socket,也就是一个文件描述符FD,失败则返回-1。
connect
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数说明:
- sockfd:一般为调用socket函数返回的文件描述符。
- *addr:服务器端的地址,格式参考bind函数。
- addrlen:*addr的长度。
- 返回值:0为成功,-1为失败。
send与write
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
函数说明:
- sockfd:文件描述符。
- *buf:要发送的内容。
- len:发送的内容的长度
- flags:标志位,默认为0,具体参考
man 2 send
。 - 返回值:成功则返回发送的内容的长度,失败则返回-1。
write函数原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
与send基本类似,唯一的区别就是少了一个flags参数,flags设置位0两者的效果一样。
另外还有个sendto,函数原型如下:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
这个函数多了一个地址和地址的长度,用于发送UDP报文,前面无需使用connect建立连接,将地址设置为NULL,长度设置为0,效果与send函数一样。
recv与read
函数原型:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
函数说明:
- sockfd:文件描述符。
- *buf:用来存放接收到的数据。
- len:*buf的长度。
- flags:标志位,具体参考
man 2 recv
。 - 返回值:成功则返回收到的内容的长度,失败则返回-1。
read函数原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
与recv基本类似,唯一的区别就是少了一个flags参数,flags设置位0两者的效果一样。
recv也有个发送UDP数据的方法,函数原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
代码实现
服务器端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
int main() {
// create socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == listenfd) {
printf("create socket error");
return -1;
}
// bind port
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if(-1 == bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr))) {
printf("bind error");
return -1;
}
// start listen
if (listen(listenfd, 2) == -1) {
printf("listem error");
return -1;
}
while (1) {
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
// accept connection
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1) {
char recvBuf[32] = {0};
// receive data
int ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
printf("recv data from client, data: %s\n", recvBuf);
// send data
ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
if (ret != strlen(recvBuf)) {
printf("send data error.");
}
} else {
printf("recv data error.");
}
close(clientfd);
}
}
//close socket
close(listenfd);
return 0;
}
启动服务器端,服务器端在地址0.0.0.0:3000启动一个侦听,客户端连接服务器成功后,给服务器发送字符串"hello";服务器收到后,将收到的字符串原封不动地发给客户端。
# gcc server.c -o server.out
# ./server.out
recv data from client, data: hello
客户端可以使用nc命令充当:
# echo hello | nc 127.0.0.1 3000
hello
客户端代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
int main() {
// create socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == clientfd) {
printf("create socket error");
return -1;
}
// connect server
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");;
serveraddr.sin_port = htons(3000);
if(-1 == connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) {
printf("connect error");
return -1;
}
// send data
int ret = send(clientfd, "hello", strlen("hello"), 0);
if (ret != strlen("hello")){
printf("send data error");
return -1;
}
// receive data
char recvBuf[32] = {0};
ret = recv(clientfd, recvBuf, 32, 0);
if (ret > 0) {
printf("receive data from server: %s", recvBuf);
} else {
printf("receive data error: %s", recvBuf);
}
// close socket
close(clientfd);
return 0;
}