>
Home

登龙(DLonng)

选择大于努力

Linux 高级编程 - Socket 编程基础(TCP,UDP)


版权声明:本文为 DLonng 原创文章,可以随意转载,但必须在明确位置注明出处!

Socket 接口简介

Socket 套接字是由 BSD(加州大学伯克利分校软件研发中心)开发的一套独立于具体协议的网络编程接口,应用程序可以用这个接口进行网络通信。要注意:Socket 不是一套通信协议(HTTP,FTP 等是通信协议),而是编程的接口,即我们在程序中使用的网络函数。TCP/IP 网络编程底层就是使用 Socket 接口来通信,所以在学习 TCP/IP 编程之前必须知道 Socket 的接口使用方法。

Socket API 接口

基本的编程接口有:socket,bind,listen,accept,connect,send,recv 等,这些函数都很重要,下面来一一学习这些函数。

创建通信套接字:socket

socket 函数创建一个通信的端点,并返回一个指向该端点的文件描述符(Linux 下一切皆是文件):

#include <sys/types.h>
#include <sys/socket.h>

/*
 * domain: 通信协议簇,例如 AF_INET, AF_UNIX...
 * type: SOCK_STREAM, SOCK_DGRAM 等等
 * protocol: 通常为 0
 * return: 成功返回文件描述符,失败返回 -1,并设置 erron
 */
int socket(int domain, int type, int protocol);

例如服务器端创建一个用于接受客户端连接的 socket 的代码:

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == server_fd) {
    perror("socket");
    exit(1);
}

分配套接字名称:bind

当用 socket 函数创建套接字后,并没有为它分配 IP 地址和端口,我们还需要使用 bind 函数来将指定的 IP 和端口分配给已经创建的 socket:

#include <sys/types.h>          
#include <sys/socket.h>

/*
 * sockfd: socket 返回的文件描述符
 * addr: 含有要绑定的 IP 和端口的地址结构指针
 * addrlen: 第二个参数的大小,使用 sizeof 来计算
 * return: 成功返回 0,失败返回 -1,并设置 erron
 */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

其实第二个参数要注意,参数指定的是 struct sockaddr * 类型,一般不直接使用这个结构,这个类型在 Linux 上有许多的变种,例如 sockaddr_in 和 sockaddr_un,经常使用后面 2 个结构定义 IP 和端口信息,在 bind 是强制转换成 struct sockaddr * 类型:

// struct sockaddr_un myaddr;
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
// 接受任何 IP 地址的连接
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 指定连接端口为 8080
myaddr.sin_port = htons(8080);
if(bind(server_fd, (struct sockaddr *)&myaddr, sizeof(sockaddr_in)) == -1) {
    perror("bind");
    exit(1);
}

开始监听:listen

使用 listen 来建立一个监听客户端连接的队列:

#include <sys/types.h>        
#include <sys/socket.h>

/*
 * sockfd: 监听的 socket 描述符
 * backlog: 建立的最大连接数
 * return: 成功返回 0,失败返回 -1,并设置 erron
 */
int listen(int sockfd, int backlog);

例如创建一个可以监听 10 个客户端连接请求的队列:

if(listen(server_fd, 10) == -1) {
    perror("listen");
    exit(1);
}

接受连接请求:accept

网络编程的核心一步就是建立客户端和服务器端的连接,使用 accept 来建立 2 者的连接:

#include <sys/types.h>   
#include <sys/socket.h>

/*
 * sockfd: 已经创建的本地正在监听的 socket
 * addr: 保存连接的客户端的地址信息
 * addrlen: sockaddr 的长度指针
 * return: 成功返回客户端的 socket 文件描述符号,失败返回 -1,设置 erron
 */
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

后两个参数我们需要定义,但是不需要初始化,在连接成功后客户端的 socket 信息会自动地填入第 3 个结构中。使用方法如下:

struct sockaddr_in clientaddr;
int clientaddr_len = sizeof(clientaddr);

// 建立连接请求
int client_fd = accept(server_fd, (struct sockaddr *)&clientaddr, &clientaddr_len);
if(client_fd == -1) {
    perror("accept error") ;
    exit(1) ;
}

// socket fd 使用完毕也必须关闭
close(client_fd);

发送数据:send,sendto

在建立连接之后,当然要发送数据,既然 socket 也是文件,发送数据其实也就是写文件,我们使用 send 函数来发送 socket 数据:

#include <sys/types.h>
#include <sys/socket.h>

/*
 * sockfd: 接受数据的 socket
 * buf: 发送的数据
 * len: 数据长度
 * flags: 当这个参数为 0,该函数等价与 write
 * return: 成功返回发送的字节数,失败返回 -1,并设置 erron
 */
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

/* sendto 功能是将数据发送到指定的地址 dest_addr,其他参数基本相同 */
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

例如服务器在建立连接后发送一个字符串到客户端:

char msg[] = "Hello Client."
send(client_fd, msg, strlen(msg), 0);

sendto(client_fd, msg, strlen(msg), 0,
      (struct sockaddr*)&dest_addr, sizeof(dest_addr));

接收数据:recv,recvfrom

既然有发送数据,必然有接收数据的函数,与 send 类似,recv 的功能也跟 read 几乎相同:

#include <sys/types.h>
#include <sys/socket.h>

/*
 * sockfd: 接收的 socket fd
 * buf: 接收缓冲区
 * len: 缓冲区长度
 * flags: 当这个参数为 0,该函数等价与 read
 * return: 成功返回接受的字节数,失败返回 -1,并设置 erron
 */
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

/* recvfrom 从指定的地址 src_addr 接收数据,其他参数与 recv 类似 */
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

例如接受服务器发送的字符串:

char msg_buf[100] = { 0 };
recv(server_fd, msg_buf, 100, 0);

int srcaddr_len = sizeof(src_addr);
recvfrom(server_fd, msg_buf, 100, 0,
        (struct sockaddr*)&src_addr, &srcaddr_len);

基本的 socket 函数就介绍完了,但是这里不可能将所有细节都列出来,想要深入的学习建议你查看对应函数的 man 手册,例如 man socketman recvfrom 等等。

使用 Socket 进行 TCP 通信

TCP 通信的概念在上一篇文章中已经介绍过了,这里使用 Socket 提供的编程接口来实际编写一个简单的服务器和客户端来模拟通信过程,下面是使用 Socket 进行 TCP 通信的过程:

tcp socket

1. TCP 服务器

其中用到的都是上面介绍的 socket 函数,整个通信过程不算很复杂,主要是:建立连接-传输或处理数据-关闭连接。下面就是一个基于 TCP 的简单服务器的例子,这里为了防止代码过多就省去了返回值检查的过程(检查过程可以参考前面的例子):

// tcp_server.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <arpa/inet.h>

int main(void) {
	int server_fd, client_fd;
	struct sockaddr_in server_addr;
	struct sockaddr_in client_addr;
	// 1. init server addr
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(8888);
	int client_addr_len = sizeof(client_addr);
	// 2. create socket
	server_fd = socket(AF_INET, SOCK_STREAM, 0);  
	// 3. bind server_addr to server_fd
	bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
	// 4. listen server_fd, max listen client num = 10
	listen(server_fd, 10);
	printf("TCP server is listening...\n");
	// 5. accept client connect
	char send_msg[] = "hello client";
	while(1) {
		client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
		// 6. send data
		send(client_fd, send_msg, sizeof(send_msg), 0);
		printf("Write \"hello client\" to client ok.\n");
		// 7. close client fd
		close(client_fd);
	}
	// 8. close server fd
	close(server_fd);
	return 0;
}

这个例子非常基础,但是还是有 4 处容易出错的地方:

  1. INADDR_ANY: 允许任何 IP 地址的客户端连接本服务器
  2. 指定端口为 8888: 连接的端口被指定为 8888,如果连接不成功则端口可能被占用,可以尝试更换端口
  3. AF_INETSOCK_STREAM: 两者确定了当前使用的是 TCP 协议
  4. 强制转换为 struct sockaddr *: 在使用 bind 和 accept 时,都需要将 sockaddr_insockaddr_un 强制转换为这个类型
  5. 不要忘记关闭 socket 的文件描述符 fd

编译运行该服务器:

gcc tcp_server.c -o tcp_server

./tcp_server
TCP server is listening...
^C

运行正常,下面来看看 TCP 客户端的代码。

2. TCP 客户端

TCP 客户端直接创建 socket,然后使用 connect 连接服务器,之后接收服务器发送的数据:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>

int main(int argc, char *argv[]) {
	if (argc < 2) {
		printf("Usage: ./tcp_client localhost\n");
		exit(1);
	}
	int server_fd = 0;
	// 1. init client addr
	struct sockaddr_in client_addr;
	client_addr.sin_family = AF_INET;
	struct hostent *myhost = gethostbyname(argv[1]);
	client_addr.sin_addr = (*((struct in_addr *)(myhost->h_addr)));
	client_addr.sin_port = htons(8888);
	// 2. create socket
	server_fd = socket(AF_INET, SOCK_STREAM, 0);
	// 3. connect server
	connect(server_fd, (struct sockaddr *)&client_addr, sizeof(client_addr));
	// 4. recv msg from server
	char msg_buf[100] = { 0 };
	recv(server_fd, msg_buf, 100, 0);
	printf("Client get server msg: %s\n", msg_buf);
	// 5. close fd
	close(server_fd);
	return 0;
}

要注意的是客户端这里根据实际的域名来获取 IP,也可以使用下面的代码直接使用指定的 IP 地址:

client_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

编译看看:

gcc tcp_client.c -o tcp_client

./tcp_client localhost

没有错误,下面来测试下。

3. 测试 TCP 连接

先运行服务器:

./tcp_server
TCP server is listening...

在新的终端中运行客户端:

./tcp_client localhost
Client get server msg: hello client

成功接收了服务器的消息,并且服务器也打印了消息:

./tcp_server
Write "hello client" to client ok.

这就成功的实现了一个简单的使用 TCP 实现服务器端和客户端通信的例子了,例子实现起来很简单,把前面介绍的 API 理解并学会使用就可以了。下面再来看看使用 UDP 的如何实现两个网络进程的通信。

使用 Socket 进行 UDP 通信

UDP 通信的基本概念也在上一篇文章中,可以点击查看,UDP 是一种面向无连接的协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频等实时性较强的数据在传送时使用 UDP 较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响,比如 QQ 就是使用的 UDP 协议。通信过程如下:

udp socket

下面是一个具体的通信例子。

1. UDP 服务器端

最后的 recvfrom 函数是从指定的地址接收 UDP 数据,与 recv 的作用基本相同。

// udp_server.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
	struct sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(8888);

	int serveraddr_len = sizeof(server_addr);
	// 1.create socket
	int server_fd = socket(AF_INET, SOCK_DGRAM, 0);
	// 2.bind
	bind(server_fd, (struct sockaddr*)&server_addr, serveraddr_len);
	// 3.recv data
	char buf[100];
	recvfrom(server_fd, buf, 100, 0,
	        (struct sockaddr*)&server_addr, &serveraddr_len);
	printf("UDP server get data from client: %s\n", buf);
	// 4.close
	close(server_fd);
	return 0;
}

2. UDP 客户端

sendto 函数是把 UDP 数据发送给指定的地址,详细使用方法参考 man sendto

// udp_client.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>

int main(int argc, char* argv[]) {
	struct hostent* myhost = gethostbyname(argv[1]);
	struct sockaddr_in client_addr;
	client_addr.sin_family = AF_INET;
	client_addr.sin_addr = *((struct in_addr*)(myhost->h_addr));
	client_addr.sin_port = htons(8888);

	// 1.socket
	int server_fd = socket(AF_INET, SOCK_DGRAM, 0);

	// 2.send data
	sendto(server_fd, "Hello World", 11, 0,
	      (struct sockaddr*)&client_addr, sizeof(client_addr));
	printf("UDP client send data ok.\n");
	close(server_fd);
	return 0;
}

3. 测试 UDP 通信

先来编译:

gcc -Wall udp_server.c -o udp_server
gcc -Wall udp_client.c -o udp_client

这里会出现些警告,我们暂时忽略,因为不影响最后的结果,但是在实际工作中还是要注意警告!再来运行 UDP 服务器端:

./udp_server

接着新开一个终端运行 UDP 客户端:

./udp_client localhost
UDP client send data ok.

数据包发送完成,回到 UDP 服务器端:

UDP server get data from client: Hello World

服务器端也成功接收了数据了,通信成功啦!

结语

这篇博客主要介绍了使用 Socket 提供的 API 进行 TCP 和 UDP 网络通信的基本方法,并实际介绍了 2 个 demo,把这两个 demo 弄清楚了,基本的 TCP,UDP 原理也就理解的差不多了,但在实际工作中我们主要还是使用优秀的开源网络库,一般不会自己封装。网络通信是一个很大的主题,很多细节没有介绍到,有兴趣可以查看 「计算机网络」 和 「unix 网络编程」 这两本经典书籍。

那我们下次见,谢谢你的阅读 :)

本文原创首发于微信公号「登龙」,分享机器学习、算法编程、Python、机器人技术等原创文章,扫码即可关注

DLonng at 09/15/17