1、常用函数介绍
int socket(int domain,int type,int protocol); /* domain:AF_INET设为IPV4 type:SOCK_STREAM对应TCP,SOCK_DGRAM对应UDP protocol:设0 返回值:返回一个套接字,失败返回-1 */
int bind(int sockfd,struct sockaddr *my_addr,int addrlen); /* sockfd:由socket()调用返回的需要绑定的套接字 my_addr:sockaddr类型的地址 addrlen:sizeof(sockaddr)。 返回值:成功返回0;失败返回-1 */
struct sockaddr_in { short sin_family; /* 地址类型,TCPIP协议只能填AF_INET */ unsigned short sin_port; /*使用端口号 */ struct in_addr sin_addr; /* 网络地址,如需绑定所有地址,填INADDR_ANY */ unsigned char sin_zero[8]; /* 填0即可 */ };//一般将其强制转换成sockaddr来使用
uint16_t htons(uint16_t hostshort); /* 把系统的16位整数调整为“大端模式” */
int inet_aton(const char *string, struct in_addr *addr); /* 把字符串的IP地址转化为in_addr结构体 */
int connect(int sockfd,struct sockaddr* serv_addr,int addrlen); /* sockfd:连接到的套接字 serv_addr:连接到的服务器地址 addrlen:sizeof(serv_addr) 返回值:失败返回-1 */
int listen(int sockfd,int backlog);//设置服务器监听模式 /* sockfd:需要设置监听的服务器套接字 backlog:进入队列中允许的连接的个数。 返回值:出错返回-1 */
int accept(int sockfd,void *addr,int* addrlen);//接受已经connect并在款冲队列中等待的套接字,队列为空时默认进入阻塞状态,直到有客户端进行connect() /* sockfd:正在监听端口的套接字 addr:用于存储客户的地址结构体 addrlen:sizeof(struct sockaddr_in) 返回值:失败-1 */
int send(int sockfd,const void* msg,int len,int flags);//TCP发送数据 /* sockfd:发送目标的套接字 msg:需要发送数据的头指针 len:数据的字节长度 flags:设为0 返回值:返回实际发送的字节数。注意,返回值可能比需要发送的字节数要少,此时需要再次发送剩下的字节。如失败返回-1 */
int recv(int sockfd,void* buf,int len,unsigned int flags);//接受TCP数据 /* sockfd:是要读取的套接口字 buf:保存数据的内存入口。 len:缓冲区的最大长度。注意,缓冲区不需用完 flags:设为0 返回值:返回实际读取到缓冲区的字节数,如果出错则返回-1。 */
2、服务器端流程
设置服务器地址
//设置一个socket地址结构server_addr,代表服务器internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); //把一段内存区的内容全部设置为0 server_addr.sin_family = AF_INET;// server_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY是全0特殊地址,用于含有多IP地址的服务器,表示同时绑定自己的所有地址 server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);//小端数转大端数函数
创建服务器套接字
//创建用于internet的流协议(TCP)socket,用server_socket代表服务器socket int server_socket = socket(AF_INET,SOCK_STREAM,0); if( server_socket < 0){ printf("Create Socket Failed!"); exit(1); }
绑定地址与套接字
//把socket和socket地址结构联系起来 if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))){ printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); exit(1); }
设置服务器为监听状态
//server_socket用于监听 if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) ){ printf("Server Listen Failed!"); exit(1); }
接受客户端的连接
int new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length); if ( new_server_socket < 0) { printf("Server Accept Failed! "); break; }
从客户端接收数据
//接收客户端发送来的信息到buffer中 length = recv(new_server_socket,buffer,BUFFER_SIZE,0);//如果对方在一次连接里发送了两次呢? if (length < 0){ printf("Server Recieve Data Failed! "); exit(1); } printf(" %s",buffer);
发送数据到客户端
char buffer[BUFFER_SIZE]; bzero(buffer, BUFFER_SIZE); strcpy(buffer,"Hello,World! 从服务器来!"); strcat(buffer," "); //C语言字符串连接 //发送buffer中的字符串到new_server_socket,实际是给客户端 send(new_server_socket,buffer,BUFFER_SIZE,0);
关闭连接,空出端口
//关闭与客户端的连接 close(new_server_socket); //关闭监听用的socket close(server_socket);
3、客户端流程
设置客户端地址
//设置一个socket地址结构client_addr,代表客户机internet地址, 端口 struct sockaddr_in client_addr; bzero(&client_addr,sizeof(client_addr)); //把一段内存区的内容全部设置为0 client_addr.sin_family = AF_INET; //internet协议族 client_addr.sin_addr.s_addr = htons(INADDR_ANY);//INADDR_ANY表示自动获取本机地址 client_addr.sin_port = htons(0); //0表示让系统自动分配一个空闲端口
建立客户端的套接字
//创建用于internet的流协议(TCP)socket,用client_socket代表客户机socket int client_socket = socket(AF_INET,SOCK_STREAM,0); if( client_socket < 0) { printf("Create Socket Failed! "); exit(1); }
绑定客户端地址与套接字
//把客户机的socket和客户机的socket地址结构联系起来 if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))){ printf("Client Bind Port Failed! "); exit(1); }
设置服务器地址结构体
//设置一个socket地址结构server_addr,代表服务器的internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); server_addr.sin_family = AF_INET; if(inet_aton(argv[1],&server_addr.sin_addr) == 0){ //服务器的IP地址来自程序的参数,aton字符串IP地址转化为网络地址格式 printf("Server IP Address Error! "); exit(1); } server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
建立于服务器的连接
socklen_t server_addr_length = sizeof(server_addr); //向服务器发起连接,连接成功后client_socket代表了客户机和服务器的一个socket连接 if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) < 0) { printf("Can Not Connect To %s! ",argv[1]); exit(1); }
接收、发送数据
关闭连接
完整代码:https://github.com/iyjhabc/study_examples/blob/master/server.c
编译c程序 gcc client.c -o client
运行客户端 ./client 127.0.0.1
127.0.0.1为保留IP地址,固定指向本地主机。
----------------------------------------------------------------------------------------------------
虽然知道各个函数怎么用,但这几个函数都是要组合起来使用,组合得不对使用就会出错,下面介绍一下如何才是正确组合TCP函数。
两个计算机通过网络连接,说白了通过两样东西来定位,IP和端口。套接字其实就是一个已经连接到某IP和某端口的一条抽象通道。知道这两个概念,下面就容易理解了。
1、服务器
作为服务器,一般来说不会首先连接别人,而是等别人主动连接它,他被动等待别人的连接。并且作为公共使用的服务器,必须有固定的端口,否则别人怎么知道怎么找到你?
(1)bind().服务器的第一步是把新建的服务器socket套接字bind一个端口。此时此套接字已经跟服务器的IP和端口紧紧联系在一起了(但还没连接)。
(2)listen()与accept().此二函数是一起使用的,首先把服务器的套接字设为监听状态,然后在循环里面调用accept。它被调用后会阻塞自己所在的线程,直到有客户端connect为止。
(3)recv().当有客户端连接服务器,accept的阻塞被释放,并返回一个已经连向该客户端的套接字。此时服务器就可以通过此套接字,使用recv函数接受来自客户端的数据,并使用send给客户端发送数据。
(4)close().accept返回的套接字使用完成后,必须使用close把它关闭。
总结来说,服务器只需自己绑定一个端口,等待客户端的连接。它完全不用管客户端的ip和端口,因为accept返回的套接字已经包含了连接向客户端IP和端口的连接通路。
2、客户端
作为客户端,一般是主动连接服务器,等待服务器的回应。客户端必须知道想要连接的服务器的IP和端口。
(1)connect().首先客户端新建一个套接字,并根据服务器的IP和端口把套接字connect到服务器,形成连接通路。
(2)send().连接成功后,就可以利用该套接字向服务器发送数据。
(3)recvfrom().因为刚才的套接字已经与服务器形成连接,因此也可以用来接收服务器返回的数据。
这里再说明一下,connect形成与服务器连接这个套接字,其实就是服务器端accept返回那个套接字,正因为如此,客户端才能用这个套接字recv服务器返回的消息。至于为什么用recvfrom而不是recv,接下来说。
服务器与客户端握手对话(客发-服收-服发-客收)代码可参考以下:
客户端:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport_client.c
服务器:https://github.com/iyjhabc/study_examples/blob/master/tcp_data_transport.c
3、recv与recvfrom
众所周知recv主要用在TCP中,recvfrom主要用在UDP中,但如客户端需要等待服务器返回的消息,再作下一步运算的话,就应使用recvfrom,如上面的例子,客户端等待服务器放置数据完成,再重新发送下一轮数据。原因是,recv默认是非阻塞的,当客户端发送完后调用recv,服务器还没发送数据过来recv就已经执行完毕,因此接受不到服务器数据。recvfrom默认是阻塞的,它会等待服务器返回的数据再往下运行。
并且recv只能用于TCP,而recvfrom同时用于TCP和UDP。recvfrom参数的地址指针将会记录接到的数据的来源地址。再有一点,recv和recvfrom参数里面的套接字,必须是已经建立连接的,没连接,怎么知道接受谁呢?要监听任意的客户端,应先使用listen和accept。
4、send
一般来说,使用一次send,便connect一次。如连续使用send,后面send的数据会丢失。为何?其实联系服务器的原理便知道。客户端connect后,服务器accept并返回一个连接双方的套接字。此时双方用此套接字发送接收数据。但一般来说服务器recv一次后便会close该套接字。如果此时客户端继续试用send,自然就发送不成功了。
总结就是,必须在套接字连同的条件下才能使用send(也是recv也是),很多情况是你自己没有断开连接,但对方其实已经close了该套接字了,便造成了发送失败。所以,连接一次发送一次数据send一次,是比较安全的做法。