linux下的socket通信

在linux下,无论多么复杂的服务器或客户端程序,无论什么编程语言实现的,其底层都离不开linux内核提供的系统调用(也就十多个函数),其网络通信的基本流程一定如下所述:

对于服务器,其通信流程一般有如下步骤:

  1. 调用socket函数创建socket,这一步会创建一个文件描述符FD。
  2. 调用bind函数将socket(也就是FD)绑定到某个ip和端口的二元组上。
  3. 调用listen函数开启侦听端口。
  4. 调用accept阻塞等待接受连接,当有客户端请求连接上来后,产生一个新的socket(客户端socket)。
  5. 基于新产生的socket调用send或recv函数开始与客户端进行数据通信。
  6. 通信结束后,调用close函数关闭客户端socket。

对于客户端,其通信流程一般有如下步骤:

  1. 调用socket函数创建客户端socket。
  2. 调用connect函数尝试连接服务器。
  3. 连接成功以后调用send或recv函数开始与服务器进行数据通信。
  4. 通信结束后,调用close函数关闭socket。

上述流程可以绘制成如下图示:

linux下的socket通信_accept

函数说明

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;
}