背景
基于C语言,对linux系统下套接字通信相关的知识点进行梳理,比如重点概念的理解,重点操作函数的解析等,最后附上相关示例代码。
概念
套接字分类
- 流式套接字(SOCK_STREAM)
- 数据报套接字(SOCK_DGRAM)
- 原始套接字
流式套接字
使用TCP(传输控制协议)进行数据传输,可以保证数据传输的准确性。
数据报套接字
使用UDP(使用者数据报协议)进行数据传输,不能保证接收的数据的准确性。
相关数据结构
struct sockaddr
#includesockaddr { unsigned short sa_family;//地址协议族 char sa_data[14];//地址(ip + port) };
struct
struct sockaddr 是通用的套接字地址,长度为16字节。
struct sockaddr_in
#include
/* Internet address. */
struct in_addr
{
uint32_t s_addr;
};
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
unsigned short sa_family;
uint16_t sin_port; /* Port number. */必须是网络字节序
struct in_addr sin_addr; /* Internet address. */必须是网络字节序
unsigned char sin_zero[8];/* Pad to size of `struct sockaddr'. */
};
internet环境下套接字的地址形式,长度也是16字节;
因为bind()函数的套接字地址类型是通用类型,所以现在通行的做法是,使用struct sockaddr_in绑定ip和端口,然后强转成struct sockaddr类型
本机转换
由于struct sockaddr_in的Ip和端口是数据需要发送到网络端,所以类型必须是网络字节序;
端口的转换需要用到下面的htons
#include
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
其实,ip的转换也可以用htonl,但入参是uint32_t,需要先将一个字符串类型的IP换算成数值类型再传参;
考虑到htonl的使用有些繁琐,一般我们使用下面的函数来进行地址的转换:
#include#include in.h> #include in_addr_t inet_addr(const char *cp); int inet_aton(const char *cp, struct in_addr *inp); char *inet_ntoa(struct in_addr in);
inet_addr()和inet_aton()都可以用于获取一个网络字节序的地址;
inet_ntoa是逆操作;
#define INADDR_ANY ((in_addr_t) 0x00000000)
INADDR_ANY是一个宏定义,数值是网络字节序,等价于inet_addr("0.0.0.0"),功能是代码所有本机IP
socket()
#include
#include
int socket(int domain, int type, int protocol);
domain 网络通信协议族,一般写AF_INET
type 通信类型,SOCK_STREAM|SOCK_DGRAM
protocol 定义额外的一个通信协议。通常只需要一个协议,所以这里填0
返回:成功返回一个可用套接字;失败返回-1,并重置errno
setsockopt
#include#include int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
主要用于设置端口复用
fcntl
#include#include int fcntl(int fd, int cmd); int fcntl(int fd, int cmd, long arg); int fcntl(int fd, int cmd, struct flock *lock);
常用于设置套接字非阻塞读
bind()
#include#include int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);
给套接字绑定到 “本机通信地址”返回:
成功返回一个可用套接字;失败返回-1,并重置errno
connect()
#include#include int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
将套接字与远程服务器通信地址绑定
返回:成功返回一个可用套接字;失败返回-1,并重置errno
listen()
int listen(int sockfd, int backlog);
sockfd一般是服务器的网络侦听套接字,backlog是连接队列的长度(等待接受连接请求)
返回:成功返0;失败返回-1并重置errno
accept()
#include#include int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
返回一个成功建立连接的新套接字
返回:成功返0;失败返回-1并设置errno
send()
#include#include int send(int s, const void *msg, size_t len, int flags); int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen); int sendmsg(int s, const struct msghdr *msg, int flags);
s 套接字
msg 待发送的数据
len 数据长度
flags 填0
close()
#includeint close(int fd);
完全关闭连接
#includeint shutdown(int sockfd, int how); how: --SHUT_RD 关闭读端 --SHUT_WR 关闭写端 --SHUT_RDWR 关闭读写(同close())
相比close,有更多的控制
示例代码
文件描述符设置阻塞与非阻塞
参考:https://www.cnblogs.com/xuyh/p/3273082.html
#include#include #include #include #include #include <string.h> /**********************使能非阻塞I/O******************** *int flags; *if(flags = fcntl(fd, F_GETFL, 0) < 0) *{ * perror("fcntl"); * return -1; *} *flags |= O_NONBLOCK; *if(fcntl(fd, F_SETFL, flags) < 0) *{ * perror("fcntl"); * return -1; *} *******************************************************/ /**********************关闭非阻塞I/O****************** flags &= ~O_NONBLOCK; if(fcntl(fd, F_SETFL, flags) < 0) { perror("fcntl"); return -1; } *******************************************************/ int main() { char buf[10] = {0}; int ret; int flags; //使用非阻塞io if(flags = fcntl(STDIN_FILENO, F_GETFL, 0) < 0) { perror("fcntl"); return -1; } flags |= O_NONBLOCK; if(fcntl(STDIN_FILENO, F_SETFL, flags) < 0) { perror("fcntl"); return -1; } while(1) { sleep(2); ret = read(STDIN_FILENO, buf, 9); if(ret == 0) { perror("read--no"); } else { printf("read = %d\n", ret); } write(STDOUT_FILENO, buf, 10); memset(buf, 0, 10); } return 0; }
Socket服务器(多进程)
#include#include #include <string.h>//memset #include #include //fork #include //socket #include in.h>//sockaddr #include //sockaddr #include //sockaddr #include //wait #define PORT 8888 #define BACKLOG 128 #define MAXLENGTH 1024 int main() { int listenfd;//侦听套接字 int new_fd;//新连接 int ret;//返回值检查 struct sockaddr_in my_addr;//本机地址 struct sockaddr_in addr;//远程地址 socklen_t addrlen; char buffer[MAXLENGTH] = {0}; listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1){ perror("socket"); return -1; } memset(&my_addr, 0, sizeof(struct sockaddr_in)); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(PORT); my_addr.sin_addr.s_addr = inet_addr("192.168.6.131"); int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt)); ret = bind(listenfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)); if (ret == -1){ perror("bind"); return -1; } ret = listen(listenfd, BACKLOG); if (ret == -1){ perror("listen"); return -1; } while (1) { addrlen = sizeof(struct sockaddr_in); new_fd = accept(listenfd, (struct sockaddr*)&addr, &addrlen); if (new_fd == -1){ perror("accept"); continue; } printf("new connection: %s\n",\ inet_ntoa(addr.sin_addr)); pid_t child = -1; child = fork(); if (child == -1){ perror("fork"); continue; } if (child == 0){ /** *子进程,与远程客户端通信 */ char *cur_addr = inet_ntoa(addr.sin_addr); while (1) { memset(buffer, 0, MAXLENGTH); ret = recv(new_fd, buffer, MAXLENGTH, 0); if (ret == -1){ perror("recv"); } else if (ret == 0){ printf("client %s close.\n", cur_addr); close(new_fd); exit(0); } else { printf("receive %dBytes data\n", ret); buffer[ret-1] = ' '; strncat(buffer, "world", 6); ret = send(new_fd, buffer, ret+6, 0); if (ret == -1){ perror("send"); } } }//---while over exit(0); } else { /** * 主进程,尝试回收子进程,并继续接收新连接。 */ pid_t child = -1; while ((child = waitpid(-1, NULL, WNOHANG)) > 0) { printf("wait child[%d] success.\n", child); } } }//---while over return 0; }
客户端
#include#include #include <string.h>//memset #include #include //fork #include //socket #include in.h>//sockaddr #include //sockaddr #include //sockaddr #include //wait #define PORT 8888 #define BACKLOG 128 #define MAXLENGTH 1024 int main() { int sockfd;//连接套接字 int ret;//返回值检查 struct sockaddr_in remote_addr;//本机地址 char buffer[MAXLENGTH] = {0}; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1){ perror("socket"); return -1; } memset(&remote_addr, 0, sizeof(struct sockaddr_in)); remote_addr.sin_family = AF_INET; remote_addr.sin_port = htons(PORT); remote_addr.sin_addr.s_addr = inet_addr("192.168.6.131"); ret = connect(sockfd, (struct sockaddr*)&remote_addr, sizeof(struct sockaddr)); if (ret == -1){ perror("connect"); return -1; } strncpy(buffer, "hello", 6); while (1) { char *stdin_ret; stdin_ret = fgets(buffer, MAXLENGTH, stdin); if (stdin_ret == NULL){ perror("fgets"); continue; } ret = send(sockfd, buffer, strlen(stdin_ret), 0); if (ret == -1){ perror("send"); } memset(buffer, 0, MAXLENGTH); ret = recv(sockfd, buffer, MAXLENGTH, 0); if (ret == -1){ perror("recv"); } else if (ret == 0){ printf("server have closed me.\n"); close(sockfd); break; } else { printf("%s\n", buffer); } }//---while over return 0; }
参考
fcntl函数详解:https://www.cnblogs.com/xuyh/p/3273082.html
多进程服务器:https://blog.csdn.net/coolwriter/article/details/80496826
socket编程:https://www.cnblogs.com/liushui-sky/p/5609535.html