概述
在刚开始学习网络套接字编程时,接触到了socket这个词。我们在以前学到Linux的时候了解到Linux下一切皆文件,并大致分为普通文件、目录文件、连接文件、设备和设备文件、管道。而套接字呢是用来实现网上的进程间的通信的,所以套接字也是文件。在TCP/IP协议中,IP地址和端口号唯一标识网络中的一个唯一进程,IP地址和端口号就是套接字。
网络字节序
网络中要实现通信,少不了数据的传输。所以这里就引入了网络字节序的概念。
在前边的学习中,我们接触到大端和小端的概念。小端:数据的地位在低地址,高位在高地址;大端:数据的低位在高地址,高位在低地址。网络数据流同样也有大端和小端之分。网络数据流先发出的是低地址,后发出的是高地址。TCP/IP规定,网络数据流采用大端字节序,即就是低位在高地址。我们之所以会说到大端和小端?是因为,网络通信的时候必须知道端口号,如果发送端是大端字节序,接收端是小端字节序,那么最后看到的端口号就是不正确的端口号,所以,我们必须将端口号在发送端和接收端之间转换成统一的字节序形式。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h表示host(主机),n表示network(网络),l表示32位长整数(即long类型),s表示16位短整数(即short类型)。例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket地址的数据结构类型
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6。然而,各种网络协议的地址格式并不相同,如下图示:
IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表中,包括16位端口号和32位IP地址。
socket编程的相关函数
套接字创建函数socket
这个函数就是用来创建套接字的。
其中,domain是表示建立的socket类型,参数如下:
至于type则是表示的是数据报传输还是字节流传输,参数如下:
最后呢protocol则表示创建的方式,一般缺省为0。
服务器绑定函数bind
这个函数用来绑定服务器的ip地址与端口号。
其中,参数sockfd表示的是服务器的套接字——也就是socket函数的返回结果。
而参数addr表示的是socket服务器的地址内容,结构体内的变量则去填写ip地址与端口号。
而sockaddr_in这个结构体成员则是如下所示:
这里呢我需要介绍一些对IP地址进行操作的函数:
其中呢我主要介绍一下inet_addr和inet_ntoa这两个,前者是将字符串表示的ip地址转成点分十进制表示的ip地址,而后者则是将一个点分十进制的ip地址转成一个字符串。
至于最后的addrlen则表示的是传入的addr的长度,可用sizeof得到。
设置监听状态函数listen
这个函数是用来设置sockfd套接字为监听状态的,用来监听客户端的连接。
其中sockfd表示要被设置的套接字,而backlog表示的是服务器链接达到最大的数量之后,还可以放到等待队列的链接个数,所以一般不要设太大。
请求连接函数connect
一般呢这个函数用于客户端,用来请求对服务器的连接。
其中呢参数sockfd表示的表示的是要链接到服务器的客户端套接字;参数addr表示的是服务器的地址与端口号;参数addrlen表示的是addr的大小一般使用sizeof得到。
单进程的套接字TCP通信
服务器:
#include<stdio.h>客户端:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int startup(const char* _ip, int _port)//建立监听套接字
{
int sock = socket(AF_INET, SOCK_STREAM, 0);//创建一个socket
if(sock < 0){
perror("socket");
exit(2);
}
//为网络协议地址赋值
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){//绑定ip与端口到套接字上面
perror("bind");
exit(3);
}
if(listen(sock, 5)){//设置监听套接字,5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
int server_sock = startup(argv[1],atoi(argv[2]));
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);//获取连接并保存连接到的客户端地址
if(client_sock < 0){
perror("accept");
continue;
}
printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));//打印连接到的ip地址和端口号
char buf[1024];
while(1){
int ret = read(client_sock, buf, sizeof(buf)-1);//服务器是先读后写,fd表示的是收到的客户端文件描述符
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("client quit\n");
close(client_sock);
break;
}else{
buf[ret] = 0;
printf("client #:%s\n", buf);
write(client_sock, buf, strlen(buf)); //读完之后将读到的数据再次返回写给客户端
}
}
close(client_sock);
}
close(server_sock);
return 0;
}
#include<stdio.h>我们在实现了上面的代码后运行一下:
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
//客户端不用绑定ip与端口号,因为是多对一的
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){用connect函数请求连接到服务器
perror("connect");
exit(3);
}
printf("connect success!\n");
char buf[1024];
while(1){
printf("client #:");//开始通信,客户端是先写后读
fflush(stdout);
int ret = read(0, buf, sizeof(buf)-1);//从标准输入中读取数据
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}
ret = read(sock, buf, sizeof(buf)-1);//再从套接字中读取数据
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}
如果不加上ip地址和端口号,就会报错
那么现在加上端口号和ip地址去运行:
先运行tcp_server,然后等待tcp_client的运行
运行tcp_client,然后就会看到连接成功
此时服务器端就会打印出客户端的ip地址和端口号。
然后客户端输入nihao,那么服务器端也会显示nihao。
多进程的套接字TCP通信
在上面呢我们实现了一个单进程TCP通信,但是呢现实生活中不可能就一个进程进行通信的,而在这之前我们也提到过多进程的概念以及多进程的编写,所以呢在这里我们也实现一下多进程TCP通信。
服务器端:
#include<stdio.h>客户端:
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
//为网络协议地址赋值
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(3);
}
if(listen(sock, 5)){//5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
int server_sock = startup(argv[1],atoi(argv[2]));
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);
if(client_sock < 0){
perror("accept");
continue;
}
printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));
pid_t id = fork();//调用fork实现多进程
if(id == 0){//child
close(server_sock);
if(fork() > 0){
}
char buf[1024];
while(1){
ssize_t s = read(client_sock, buf, sizeof(buf)-1);
if(s > 0){
buf[s]=0;
printf("client #: %s\n", buf);
}else if(s == 0){
printf("client quit\n");
break;
}else{
perror("read");
exit(5);
}
}
close(client_sock);
}else{//father
close(client_sock);
while(waitpid(-1, NULL, WNOHANG));
continue;
}
}
return 0;
}
#include<stdio.h>多进程的特点就在于调用fork()函数处理了一下,让子进程去走,而父进程一直等待。
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("connect");
exit(3);
}
printf("connect success!\n");
char buf[1024];
while(1){
printf("client #:");
fflush(stdout);
int ret = read(0, buf, sizeof(buf)-1);
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}
ret = read(sock, buf, sizeof(buf)-1);
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}
服务器端可以创建多个子进程去处理客户端发来的信息。当每次收到一个新的客户端的连接请求的时候,我们就会fork()出一个子进程,父进程用于等待子进程,子进程用于执行 读客户端发的数据 的操作。细心的你可能会发现,我们在子进程读取信息之前还进行了一次fork(),这是为什么呢?其实,我们用子进程fork()出一个孙子进程,终止掉儿子进程,儿子进程被它的父进程回收,此时的孙子进程就是一个孤儿进程,被1号进程领养。这样做的目的就是,不要让儿子进程等待孙子进程太久而消耗太多的系统资源。
另外,这里还涉及到父进程关闭通信套接字,子进程关闭监听套接字。这是因为,父进程是来监听的,不需要通信,子进程是读取信息的,不需要监听。
这里呢运行结果我就不显示了,如果有兴趣你可以自己去试一下。
多线程的套接字TCP通信
上面已经提到了多进程,那么这里我们就不得不提到多线程了。以前的学习中我们知道线程是系统内部的一个执行流,是在进程的地址空间中运行的。而进程是程序的一次动态执行过程,系统中进程过多的话会增加系统的负担,所以就有了多线程的通信。
服务器端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>
static void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int startup(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);//htons就是将host主机转换为net网络中的,而s代表short,l则代表long
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("bind");
exit(3);
}
if(listen(sock, 5)){//5表示监听的最大个数
perror("listen");
exit(4);
}
return sock;
}
void* thread_handler(void* arg)//为每一个线程执行客户端的读写
{
int sock = (int)arg;
printf("sock:%d\n", sock);
char buf[1024];
while(1){
ssize_t s = read(sock, buf,sizeof(buf)-1);
if(s > 0){
buf[s]=0;
printf("client #: %s\n", buf);
if(write(sock, buf, sizeof(buf)-1) < 0){
break;
}
}else if(s == 0){
printf("client quit\n");
break;
}else{
perror("read");
break;
}
}
close(sock);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
int server_sock = startup(argv[1],atoi(argv[2]));
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &len);
if(client_sock < 0){
perror("accept");
continue;
}
printf("IP is %s, Port is %d\n", inet_ntoa(client_addr.sin_addr), htons(client_addr.sin_port));
pthread_t tid;
int ret = pthread_create(&tid, NULL, thread_handler, (void*)(client_sock));//创建一个新线程来执行客户端的读写操作
if(ret < 0){
perror("pthread_create");
exit(5);
}
pthread_detach(tid);//设置线程状态为分离状态,这样主线程不用去等待,可以继续执行循环
}
close(server_sock);
return 0;
}
客户端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
void usage(const char* proc)
{
printf("Usage: %s [local_ip] [local_port]\n", proc);
}
int main(int argc, char* argv[])
{
if(argc != 3){
usage(argv[0]);
exit(1);
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(connect(sock, (struct sockaddr*)&local, sizeof(local)) < 0){
perror("connect");
exit(3);
}
printf("connect success!\n");
char buf[1024];
while(1){
printf("client #:");
fflush(stdout);
int ret = read(0, buf, sizeof(buf)-1);
if(ret <= 0){
perror("read");
exit(4);
}else{
buf[ret-1] = 0;
write(sock, buf, strlen(buf));
}
ret = read(sock, buf, sizeof(buf)-1);
if(ret < 0){
perror("read");
exit(5);
}else if(ret == 0){
printf("server quit\n");
break;
}else{
buf[ret] = 0;
printf("server #:%s\n", buf);
}
}
close(sock);
return 0;
}
多线程呢就是在主线程中创建出一个新线程,新线程的执行函数是读取信息。类似于上边的多进程间的通信,我们可以将新的线程进行分离,分离之后的线程就不需要主线程去等待,而是由操作系统区回收。(这里我们不可以join新线程,如果这样做的话,主线程还是需要花费很长的时间去等待,所以,新的线程还是由系统去回收)
补充:server bind失败的原因?
这里呢我们来模拟一种情形:先运行server,再运行client,client给server发数据,然后ctrl+c终止调server,立即再次启动server,会出现什么现象呢?
我们可以看到Address already is in use。
这是什么原因呢?服务器终止程序,服务器就是主动发起断开连接请求的一方,根据TCP的3次握手4次挥手协议(如果有些不清楚请点击这里查看tcp协议的三次握手与四次挥手),主动发起连接断开请求的一方,最后必须等待2MSL的时间确认客户端是否收到自己的确认信息。这里,我们立即运行server的时候,server还是在TIME_WAIT状态,所以bind的时候就会出现地址已经被占用。
解决方法:socket之后,bind之前,加语句
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
就是为了处理这个问题。
其中的sock就是socket函数创建套接字的返回值。