Linux Socket 网络编程重要函数用法及注意事项 {ignore}
文章目录
- Linux Socket 网络编程重要函数用法及注意事项 {ignore}
- Socket TCP 系统调用概述
- 函数详细说明
- int socket (int domain,int type,int protocol)
- int bind (int sockfd,struct sockaddr * my_addr,int addrlen)
- int listen (int sockfd, int backlog)
- int accept (int s,struct sockaddr * addr,int * addrlen)
- int connect (int sockfd, const struct sockaddr * serv_addr, int addrlen)
- int recv (int sockfd, void *buf, int len, unsigned int flags)
- int send (int sockfd, const void *buf, int len, unsigned int flags)
- int close (int fd)
- 服务端多线程接收数据实例
Socket TCP 系统调用概述
TCP三次握手、四次挥手和连接状态切换图:
https://www.cnblogs.com/Victor-Tian/p/7842501.html
服务端: socket() -> bind() -> listen() -> accept() -> recv() -> send() -> close()
客户端: socket() -----------> connect() ------------> send() -> recv() -> close()
服务端函数作用
-
socket()创建socket,生成文件描述符。
注意:在服务端创建完成socket之后,可以设置socket的SO_REUSEADDR选项,因为Linux中,如果主机上有任何可匹配到本地端口的TCP连接,则本地端口不能被重复使用,即bind()一个已经使用的socket会失败。 该选项可解放该限制,大多数TCP服务器都应该开启这个选项。具体可参考实例。 -
bind()将刚创建的socket绑定到本地地址上。
注意:如果绑定的本地端口已经被使用,则会失败。通过设置socket的SO_REUSEADDR选项可避免该问题。
造成绑定失败的原因可能如下。
- 之前服务器的连接经过close()、或者直接崩溃,使得TCP结点处于TIME_WAIT状态,直到2*MSL超时后,才能重新绑定。
-
listen()将socket置为侦听状态。
注意:此时,listen状态下的socket已经可以接收来自客户端的connect了。 -
accept()等待客户端连接connect。
注意:默认设置下accept()会阻塞进程,直到有客户端connect后才继续执行后续程序。 -
recv()接收客户端发送的数据。
注意:默认设置下,阻塞进程,直到接收到数据。
也可用read()函数来接收数据。 -
send()发送数据到客户端。
也可用write()函数来发送数据。 -
close()关闭连接。
注意:关闭连接够,连接经过“四次挥手”最终进入TIME_WAIT状态,正常的TIME_WAIT状态要等待2*MSL时间后,才会真正关闭连接。在socket未设置SO_REUSEADDR时,当有TCP结点处于TIME_WAIT状态时,是无法通过该结点创建新的连接。
TIME_WAIT作用如下。
- 实现可靠的连接终止,在必要时重传最后一帧ACK包。
- 让老的重复报文段在网络中过期失效。如果无TIME_WAIT,close()后立刻创建新的相同socket结点,那么,网络中老的重复报文段会在新连接中被当做合法报文处理。
客户端函数作用
-
socket()创建socket,生成文件描述符。
-
connect()连接到指定地址、端口的 处于listen或者connect状态下的服务端结点。
-
send()发送数据到服务端。
也可用write()函数来发送数据。 -
recv()接收服务端的数据。
也可用read()函数来接收数据。
函数详细说明
下列函数的详细参数说明,可参考:
http://net.pku.edu.cn/~yhf/linux_c/
int socket (int domain,int type,int protocol)
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 创建socket,生成文件描述符
* @Para : int domain 指定地址类型,常用如下几种
* AF_UNIX 进程间socket通信协议
* AF_INET Ipv4网络协议
* AF_INET6 Ipv6网络协议
* int type 指定socket类型,常用如下几种
* SOCK_STREAM TCP类socket
* SOCK_DGRAM UDP类socket
* int protocal 用来指定socket所使用的传输协议编号,通常此参考不用管它,设为0即可。
*
* @return : 成功返回socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int socket(int domain,int type,int protocol);
int bind (int sockfd,struct sockaddr * my_addr,int addrlen)
注意:如果绑定的本地端口已经被使用,则会失败。通过设置socket的SO_REUSEADDR选项可避免该问题。
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 将socket创建的socket结点,绑定到本地的IP、端口上。
* @Para : int sockfd 服务端的文件描述符,由服务端创建socket时生成。
* const struct sockaddr * my_addr 存储本地IP地址、端口号等信息的结构体指针,为了兼容IPv4 IPv6,本处使用了兼容模式的结构体。具体用法参考实例代码。
* int addrlen 存储本地地址信息的my_addr结构体缓存区长度。(由于IPv4 IPv6等地址协议类型不同,会造成结构体存储的内容不同)
*
* @return : 成功返回socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int bind (int sockfd,const struct sockaddr * my_addr, int addrlen);
/*为了兼容IPv4 IPv6的结构体*/
struct sockaddr
{
unsigned short int sa_family; //AF_INET(IPv4) 或 AF_INET6(IPv6)
char sa_data[14];
};
/*IPv4 协议的地址参数结构体*/
struct socketaddr_in
{
unsigned short int sin_family; //AF_INET 只能能是IPv4类型
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr;
};
/*IPv6 协议的地址参数结构体*/
struct sockaddr_in6
{
uint16_t sin6_family; /* Address family AF_INET6 */ //只能是IPv6类型
uint16_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
struct in6_addr
{
uint8_t s6_addr[16]; /* IPv6 address */
};
int listen (int sockfd, int backlog)
注意:此时,listen状态下的socket已经可以接收来自客户端的connect了,在此之前的客户端connect均会被忽略
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 将服务端socket创建的结点置为listen状态,可以接收来自客户端的connect。
* @Para : int sockfd 要设置为listen状态的服务端socket的文件描述符
* int backlog 缓冲客户端的connect请求的数量。由于listen状态下就已经可以接收客户端的connect,
* 但必须要服务端进入accept()才能处理,此参数可设置最多缓存待处理的connect的个数。
*
* @return : 成功返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int listen(int sockfd, int backlog);
int accept (int s,struct sockaddr * addr,int * addrlen)
注意:该函数中 int * addrlen 既是一个输入参数,也是一个输出参数,在调用accept()之前,一定要初始化指针指向的内容值。详见函数参数注释。
另外,该函数会阻塞进程,直到接收到客户端连接connect。
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 用来接收参数sockfd的socket连线,参数sockfd的socket必需先经bind()、listen()函数处理过。
* @Para : int sockfd 服务端的文件描述符,由服务端创建socket时生成。
* struct sockaddr * addr 接受到客户端的connect后,存储客户端IP地址、端口等信息的结构体指针
* int * addrlen 既是输入参数,也是输出参数,调用accept()之前,要将该指针指向的值设置为addr的缓冲区长度,
* accept()返回后,这个值存储了复制到addr缓存区中的数据字节数(即返回的客户端IP、端口信息长度)。
*
* @return : 成功返回客户端的socket的文件描述符,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int accept(int sockfd,struct sockaddr * addr,int * addrlen);
int connect (int sockfd, const struct sockaddr * serv_addr, int addrlen)
注意:
如果connect()失败或者connect()成功之后又断开了连接,希望重新建立连接,可移植的方式是,先关闭失败或者断开连接的客户端socket,创建一个新的socket,在新socket上重新建立连接。
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 用于客户端连接服务端。
* @Para : int sockfd 客户端中由socket创建的结点的文件描述符
* const struct sockaddr * addr 要连接到的服务端IP地址、端口等信息的结构体指针
* int addrlen addr的结构体数据长度。
*
* @return : 成功返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int connect(int sockfd, const struct sockaddr * addr, int addrlen);
int recv (int sockfd, void *buf, int len, unsigned int flags)
默认接收时未收到数据会阻塞进程,将flags设置为MSG_DONTWAIT可调整为非阻塞执行,非阻塞情况下,若未接收到数据,则立即报错返回,错误代码在erron中。
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 用于接收远端的数据。
* @Para : int sockfd 远端结点的文件描述符
* void * buf 接收到的数据缓存区
* int len 缓存区buf的长度
* unsigned int flags 接收配置选项,一般设置为0。详细说明参考本节开头的网址
*
* @return : 成功返回接收到的字节数,接收到文件结束符EOF时返回0,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int recv (int sockfd, void *buf, int len, unsigned int flags);
int send (int sockfd, const void *buf, int len, unsigned int flags)
默认发送缓存区满的时候会阻塞进程,将flags设置为MSG_DONTWAIT可调整为非阻塞执行,非阻塞情况下,发送时若发送缓存以满,则立即报错返回,错误代码在erron中。
#include<sys/types.h>
#include<sys/socket.h>
/*
* @Description: 用于发送数据到远端。
* @Para : int sockfd 远端结点的文件描述符
* const void * buf 要发送的数据缓存区
* int len 缓存区中要发送的数据长度
* unsigned int flags 发送配置选项,一般设置为0。详细说明参考本节开头的网址
*
* @return : 成功返回发送的字节数,失败返回-1,失败的错误码在errno中,错误码详细说明参考本节开头的网址
**/
int send (int sockfd, const void *buf, int len, unsigned int flags);
int close (int fd)
#include<unistd.h>
close为双端关闭,调用后,不能发送,也不能接收。若要实现单端关闭,将全双工变为单工,则可参考shutdown()函数功能。
close关闭后,TCP会经历四次挥手过程,进入TIME_WAIT状态,经过2*MSL时间后,才会真正关闭连接。
/*
* @Description: 用于发送数据到远端。
* @Para : int fd 要关闭的socket结点文件描述符
*
* @return : 若文件顺利关闭则返回0,发生错误时返回-1
**/
int close(int fd);
服务端多线程接收数据实例
头文件 socket_server.h 代码如下
#ifndef _SOCKET_SERVER_H_
#define _SOCKET_SERVER_H_
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <string>
#include <pthread.h>
using namespace std;
#define MAX_WAIT_CONNECTION 20 //进入侦听状态后,服务端所能缓存的最多connect请求数量;
#define REVBUFFLEN 200
typedef struct serverparam
{
int client_fd; //客户端文件表示符
unsigned int order; //该节点在vector中的序号
pthread_t client_pthread_id; //客户端接收任务的任务id
socklen_t client_struct_len; //客户端结构长度
struct sockaddr_in client_addr; //客户端参数
char revbuf[REVBUFFLEN]; //接受缓存区
}SRVERPARAM;
void * server_accept(void *);
void * server_rev(void * arg);
void Ctrl_C_fun(int sig);
void DeleteClient(SRVERPARAM * ClientNow);
#endif
服务端具体实现文件 socket_server.cpp
#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include "socket_server.h"
#include <iostream>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
int server_fd; //服务端文件标识符
struct sockaddr_in server_addr; //服务端地址结构
int ClientSum = 0; //系统中与服务端连接的客户端总数
int main(void)
{
pthread_t server_pthread_id;
server_addr.sin_family = AF_INET; //IPv4协议
server_addr.sin_port = htons(8000);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY) ; //inet_addr("192.168.127.131")
server_fd = socket( server_addr.sin_family, SOCK_STREAM, IPPROTO_TCP);
if(server_fd == -1)
{
printf("socket create failed!\r\n");
return 0;
}
int optval = 1;
if(setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
printf("socket SO_REUSEADDR option set failed!\r\n");
if(bind(server_fd, (struct sockaddr *)&server_addr, (socklen_t)sizeof(struct sockaddr_in)) == -1)
{
printf("bind failed!\r\n");
return 0;
}
if(listen(server_fd, MAX_WAIT_CONNECTION) == -1)
{
printf("listen failed!\r\n");
return 0;
}
pthread_attr_t server_accept_attr;
if(pthread_create(&server_pthread_id, NULL, server_accept, NULL) == -1)
{
printf("server_accept pthread create failed\r\n");
return 0;
}
while (1)
{
/* code */
}
close(server_fd);
return 0;
}
/*
* @Description: pthread of server accept client connect.
* @Para : NULL
* @return : NULL
* @Author : Huge
* @Data : 2020.02.10 22:50
**/
void * server_accept(void * arg)
{
printf("Waiting for client connect!\r\n");
pthread_detach(pthread_self()); //线程退出后自行删除线程资源
while (1)
{
SRVERPARAM * ClientNow = (SRVERPARAM *)malloc(sizeof(SRVERPARAM)); //为客户端申请临时空间
if(ClientNow == NULL)
{
cout << "server_accept malloc SRVERPARAM failed!\r\n" << endl;
continue;
}
ClientNow -> client_struct_len = sizeof(struct sockaddr); //使用accept之前一定要初始化
ClientNow -> client_fd = accept(server_fd, (struct sockaddr *)&(ClientNow -> client_addr), &(ClientNow -> client_struct_len));
if(ClientNow -> client_fd == -1)
continue;
struct hostent * client_host = NULL;
client_host = gethostbyaddr(&ClientNow -> client_addr.sin_addr, 4, AF_INET);
if(client_host == NULL)
printf("Get Client host failed!\r\n");
else
printf("Client host name %s\r\n", client_host -> h_name);
if(pthread_create(&ClientNow -> client_pthread_id, NULL, server_rev, (void *)ClientNow) == -1)
{
free(ClientNow);
printf("server_rev pthread create failed\r\n");
}
else
{
ClientSum++;
printf("Client IP %s port %d.\r\n", inet_ntoa(ClientNow -> client_addr.sin_addr) , ClientNow -> client_addr.sin_port);
printf("Connection sum is %d\r\n", ClientSum);
}
}
return NULL;
}
/*
* @Description: pthread of server receive data from client.
* @Para : the struct SRVERPARAM address of client.
* @return : NULL
* @Author : Huge
* @Data : 2020.02.10 22:50
**/
void * server_rev(void * arg)
{
pthread_detach(pthread_self()); //线程退出后自行删除线程资源
SRVERPARAM * ClientNow = (SRVERPARAM *)arg;
while (1)
{
struct timeval timeout={5,0}; //接收的timeout延时为5s
int ret = setsockopt(ClientNow -> client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout,sizeof(timeout));
if(ret != 0)
{
DeleteClient(ClientNow);
break;
}
int revlen = recv(ClientNow -> client_fd, ClientNow -> revbuf, REVBUFFLEN - 1, 0);
if(revlen == -1) //超时
{
switch (errno)
{
case EAGAIN:
{
printf("Waiting IP %s PORT %d data timeout!\r\n",
inet_ntoa(ClientNow -> client_addr.sin_addr),
ClientNow -> client_addr.sin_port);
break;
}
default:
printf("recv IP %s PORT %d data failed!\r\n",
inet_ntoa(ClientNow -> client_addr.sin_addr),
ClientNow -> client_addr.sin_port);
break;
}
DeleteClient(ClientNow);
break; //退出线程,删除该线程的接收任务
}
else //正常接收
{
ClientNow -> revbuf[revlen] = '\0';
printf("IP %s PORT %d : %s\r\n",
inet_ntoa(ClientNow -> client_addr.sin_addr),
ClientNow -> client_addr.sin_port,
ClientNow -> revbuf);
}
}
return NULL;
}
/*
* @Description: Delete useless client connection, and free the malloc RAM.
* @Para : the struct SRVERPARAM address of client.
* @return : void
* @Author : Huge
* @Data : 2020.02.10 22:50
**/
void DeleteClient(SRVERPARAM * ClientNow)
{
close(ClientNow -> client_fd);
ClientSum--;
printf("Client IP %s PORT %d closed! connection sum %d\r\n",
inet_ntoa(ClientNow -> client_addr.sin_addr),
ClientNow -> client_addr.sin_port,
ClientSum);
free(ClientNow);
}