Linux C 网络编程

2017-08-21 language linux c/cpp

获取地址

getpeername() 用于获取与某个套接字关联的对端地址,accept() 在接收连接的时候也会获取对端的地址,getsockname() 用于获取本地地址。

// Server
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

int main()
{
	int svrfd, clifd;
	struct sockaddr_in addr;

	svrfd = socket(AF_INET, SOCK_STREAM, 0);
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr =htonl(INADDR_ANY);
	addr.sin_port = htons(8888);

	bind(svrfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in));
	listen(svrfd, 5);

	struct sockaddr_in addrcli, peeraddr;
	socklen_t len = sizeof(struct sockaddr_in);
	clifd = accept(svrfd, (struct sockaddr *)&addrcli, &len);

	printf("Client #%d ip=%s port=%d\n", clifd,
		inet_ntoa(addrcli.sin_addr), ntohs(addrcli.sin_port));

	len = sizeof(struct sockaddr_in);
        getpeername(clifd, (struct sockaddr *)&peeraddr, &len);
        printf("Peer #%d ip=%s port=%d\n", clifd,
		inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

	getchar();
	close(svrfd);
	close(clifd);
	return 0;
}
// Client
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>

int main()
{
	int rc;
	int sockfd;

	struct sockaddr_in addr;
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8888);

	printf("Server ip=%s port=%d\n",
		inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	rc = connect(sockfd, (const struct sockaddr *)&addr, sizeof(struct sockaddr_in));

	struct sockaddr_in svraddr;
	socklen_t len = sizeof(struct sockaddr_in);
	getsockname(sockfd,(struct sockaddr *)&svraddr, &len);
	printf("Local #%d ip=%s port=%d\n", sockfd,
		inet_ntoa(svraddr.sin_addr), ntohs(svraddr.sin_port));

	getchar();
	close(sockfd);
	return 0;
}

直接编译运行,然后通过 netstat -atunp | grep 8888 查看。

Unix

如果使用的是 Unix Domain Socket 那么可以获取到对端的 PID UID GID 信息,可以用来判断对方是否有相应的权限。

客户端在发送的时候也可以指定用户,但是必须要有 CAP_SYS_ADMIN 权限才可以。

struct ucred cred;
socklen_t len;
len = sizeof(struct ucred);
getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &cred, &len);
printf("Credentials from SO_PEERCRED: pid=%d, uid=%d, gid=%d\n", cred.pid, cred.uid, cred.gid);

非阻塞链接

Windows 平台上无论利用 socket() 函数还是 WSASocket() 创建的 socket 都是阻塞模式的,而 Linux 上可以在创建 Socket 时将其指定为异步,例如。

socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);

如果已经创建了 Socket (实际上其它的描述符也是支持的),还可以通过以下 API 函数来设置。

Linux 平台上可以调用 fcntl() 或者 ioctl() 函数,实例如下:

fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);  
ioctl(sockfd, FIONBIO, 1);  /* 1: NONBLOCK, 0: BLOCK */

Accept

默认,Windows 和 Linux 上的 accept() 函数返回的 socekt 也是阻塞的,而 Linux 另外提供了一个 accept4() 函数,可以直接将返回的 socket 设置为非阻塞模式。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

只要将 accept4() 最后一个参数 flags 设置成 SOCK_NONBLOCK 即可。

Recv & Send

实际上在接收和发送数据的时候,Linux 中也就对应了 send() sendto() recv() recvfrom() 之类的函数,可以在调用的时候将其中的 flags 设置为 MSG_DONTWAIT 即可。

建立链接

对于面向连接的 socket 类型,如 SOCK_STREAM,在通过 connect() 函数建立链接时,对于 TCP 需要三次握手过程。

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

正常返回 0 ,失败返回 -1 并设置 errno 标示失败的原因,常见原因是主机不可达或超时。

对于 TCP 套接字,在通过 connect() 函数建立链接时需要经过三次握手,Linux 内核默认的超时时间是 75s ,如果是阻塞模式,那么 connect() 会等到链接建立成功或者超时失败,这也就导致如果服务异常,每个客户端都要等待 75s 才可能退出。

使用非阻塞 connect 时需要注意的问题是:

  1. 很可能 调用 connect 时会立即建立连接(比如,客户端和服务端在同一台机子上),必须处理这种情况。
  2. POSIX 定义了两个与非阻塞相关的内容:
    • 成功建立连接时,socket 描述字变为可写,报错可以通过 getsockopt() 获取。
    • 连接建立失败时,socket 描述字既可读又可写同时报错,可以优先检查报错退出。
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/select.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int select_version(int sockfd)
{
	fd_set rset, wset;

	FD_ZERO(&rset);
	FD_ZERO(&wset);
	FD_SET(sockfd, &rset);
	FD_SET(sockfd, &wset);

	struct timeval tval;
	tval.tv_sec = 0;
	tval.tv_usec = 300 * 1000;

	int rdynum;
	rdynum = select(sockfd + 1, &rset, &wset, NULL, &tval);
	if (rdynum < 0) {
		fprintf(stderr, "Select error, %s\n", strerror(errno));
		return -1;
	} else if (rdynum == 0) {
		fprintf(stderr, "Select timeout\n");
		return -1;
	}

	if (FD_ISSET(sockfd, &rset)) {
		fprintf(stderr, "read \n");
	}

	if (FD_ISSET(sockfd, &wset)) {
		fprintf(stderr, "write \n");
	}

	return 0;
}

int epoll_version(int sockfd)
{
	int ep, i, evtnum;

	ep = epoll_create(1024); /* ignore 1024 since Linux 2.6.8 */
	if (ep < 0) {
		fprintf(stderr, "Create epoll error, %s\n", strerror(errno));
		return -1;
	}

	struct epoll_event event;
	event.events = EPOLLIN | EPOLLOUT | EPOLLET;
	event.data.fd = sockfd;
	epoll_ctl(ep, EPOLL_CTL_ADD, sockfd, &event);


	int err = 0;
	int errlen = sizeof(err);

	struct epoll_event events[64];
	evtnum = epoll_wait(ep, events, sizeof(events), 60 * 1000);
	for (i = 0; i < evtnum; i++) {
		int fd = events[i].data.fd;
		if (events[i].events & EPOLLERR) {
			fprintf(stderr, "error\n");

			if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == -1) {
				fprintf(stderr, "getsockopt(SO_ERROR): %s", strerror(errno));
				close(fd);
				//return -1;
			}

			if (err) {
				fprintf(stderr, "%s\n", strerror(err));
				close(fd);
				//return -1;
			}
		}

		if (events[i].events & EPOLLIN) {
			fprintf(stderr, "in\n");
		}

		if (events[i].events & EPOLLOUT) {
			fprintf(stderr, "out\n");
		}
	}

	return 0;
}


int main(int argc, char** argv)
{
	struct sockaddr_in svraddr;
	int sockfd, rc;


	memset(&svraddr, 0, sizeof(svraddr));
	svraddr.sin_family = AF_INET;
	svraddr.sin_port = htons(8009);
	inet_aton("127.0.0.1", &svraddr.sin_addr.s_addr);

	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		perror("Create socket fail");
		return -1;
	}

	if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK) < 0) {
		perror("Set socket O_NONBLOCK fail");
		return -1;
	}

	rc = connect(sockfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr_in));
	if (rc < 0 && errno != EINPROGRESS) {
		perror("Connect remote server fail");
		return -1;
	}

	//select_version(&c_fd);
	epoll_version(sockfd);

	close(sockfd);

	return 0;
}